弄清楚類與對象的本質與基本特征,是進一步學習面向對象編程語言的基本要求。面向對象程式設計與面向過程程式設計在思維上存在著很大差別,改變一種思維方式並不是一件容易的事情。 ...
弄清楚類與對象的本質與基本特征,是進一步學習面向對象編程語言的基本要求。面向對象程式設計與面向過程程式設計在思維上存在著很大差別,改變一種思維方式並不是一件容易的事情。
一、面向對象程式設計
程式由對象組成,對象包含對用戶公開的特定功能部分,和隱藏在其內部的實現部分。從設計層面講,我們只關心對象能否滿足要求,而無需過多關註其功能的具體實現。面對規模較小的問題時,面向過程的開發方式是比較理想的,但面對解決規模較大的問題時,面向對象的程式設計往往更加合適。
類
對象是對客觀事物的抽象,類是對對象的抽象,是構建對象的模板。由類構造(construct)對象的過程稱為創建類的實例(instance)或類的實例化。
封裝是將數據和行為組合在一個包中,並對使用者隱藏數據的實現方式。對象中的數據稱為實例域(instance field)或屬性、成員變數,操縱數據的過程稱為方法(method)。對象一般有一組特定的實例域值,這些值的集合就是對象當前的狀態。封裝的關鍵在於不讓類中的方法直接的訪問其他類的實例域,程式僅通過對象的方法與對象數據進行交互。封裝能夠讓我們通過簡單的使用一個類的介面即可完成相當複雜的任務,而無需瞭解具體的細節實現。
對象的三個主要特征
- 對象的行為(behavior):可以對對象施加哪些操作,通過方法(method)實現。
- 對象的狀態(state):存儲對象的特征信息,通過實例域(instance field)實現。
- 對象的標識(identity):辨別具有不同行為與狀態的不同對象。
設計類
傳統的面向過程的程式設計,必須從頂部的 main
入口函數開始編寫程式。面向對象程式設計沒有所謂的頂部,我們要從設計類開始,然後再往每個類中添加方法。那麼我們該具體定義什麼樣的類?定義多少個?每個類又該具備哪些方法呢?這裡有一個簡單的規則可以參考 —— “找名詞與動詞”原則。
我們需要在分析問題的過程中尋找名詞和動詞,這些名詞很有可能成為類,而方法對應著動詞。當然,所謂原則,只是一種經驗,在創建類的時候,哪些名詞和動詞是重要的,完全取決於個人的開發經驗(抽象能力)。
類之間的關係
最常見的關係有:依賴(use-a)、聚合(has-a)、繼承(is-a)。可以使用UML(unified modeling language)繪製類圖,用來可視化的描述類之間的關係。
二、預定義類與自定義類
在 Java 中沒有類就無法做任何事情,Java 標準類庫中提供了很多類,這裡稱其為預定義類,如 Math 類。要註意的是:並非所有類都具有面向對象的特征(如 Math 類),它只封裝了功能,不需要也不必要隱藏數據,由於沒有數據,因此也不必擔心生成以及初始化實例域的相關操作。
要使用對象,就必須先構造對象,並指定其初始狀態。我們可以使用構造器(constructor)構造新實例,本質上,構造器是一種特殊的方法,用以構造並初始化對象。構造器的名字與類名相同。如需構造一個類的對象,需要在構造器前面加上 new
操作符,如new Date()
。通常,希望對象可以多次使用,因此,需要將對象存放在一個變數中,不過要註意,一個對象變數並沒有實際包含一個對象,而僅僅是引用一個對象。
訪問器與修改器 我們把只訪問對象而不修改對象狀態的方法稱為 訪問器方法
(accessor method)。如果方法會對對象本身進行修改,我們稱這樣的方法稱為 更改器方法
(mutator method)。
用戶自定義類
要想創建一個完成的程式,應該將若幹類組合在一起,其中只有一個類有 main 方法。其它類( workhorse class)沒有 main 方法,卻有自己的實例域和實例方法,這些類往往需要我們自己設計和定義。
一個源文件中,最多只能有一個公有類(訪問級別為public),但可以有任意數目的非公有類。儘管一個源文件可以包含多個類,但還是建議將每一個類存在一個單獨的源文件中。 不提倡用public
標記實例域(即對象的屬性),public
數據域允許程式中的任何方法對其進行讀取和修改。當實例域設置為 private
後,如果需要對其進行讀取和修改,可以通過定義公有的域訪問器或修改器來實現。這裡要註意:不要編寫返回引用可變對象
的訪問器方法,如:
class TestClass{
private Date theDate;
public getDate(){
return theDate; // Bad
}
}
上面的訪問器返回的是對實例屬性 theDate
的引用,這導致在後續可以隨意修改當前實例的 theDate
屬性,比如執行x.getDate().setTime(y)
,破壞了封裝性!如果要返回一個可變對象的引用,應該首先對他進行克隆,如下:
class TestClass{
private Date theDate;
public getDate(){
return (Date) theDate.clone(); // Ok
}
}
構造器
構造器與類同名,當實例化某個類時,構造器會被執行,以便將實例域初始化為所需的狀態。構造器總是伴隨著 new
操作符的調用被執行,不能對一個已經存在的對象調用構造器來重置實例域。
- 構造器與類同名
- 每個類可以有多個構造器
- 構造器可以有 0 個或多個參數
- 構造器沒有返回值
- 構造器總是伴隨著
new
操作一起調用
基於類的訪問許可權
方法可以訪問所屬類的所有對象的私有數據。[*]
在實現一個類時,應將所有的數據域都設置為私有的。多數時候我們把方法設計為公有的,但有時我們希望將一個方法劃分成若幹個獨立的輔助方法,通常這些輔助方法不應該設計成為公有介面的一部分,最好將其標記為 private
。只要方法是私有的,類的設計者就可以確信:他不會被外部的其他類操作調用,可以將其刪去,如果是公有的,就不能將其刪除,因為其他的代碼可能依賴它。
final 實例域
在構建對象時必須對聲明的 final 實例域進行初始化,就是說必須確保在構造器執行之後,這個域的值被設置,並且在後面的操作中,不能夠再對其進行修改。final
修飾符大都用於基本類型,或不可變類的域。
靜態域和靜態方法
靜態域和靜態方法,是屬於類且不屬於對象的變數和函數。
通過 static
修飾符,可以標註一個域為靜態的,靜態域屬於類,而不屬於任何獨立的對象,但是每個對象都會有一份這個靜態域的拷貝。靜態方法是一種不能對對象施加操作的方法,它可以訪問自身類的靜態域,類的對象也可以調用類的靜態方法,但更建議直接使用類名調用靜態方法。
使用靜態方法的場景 : 一個方法不需要訪問對象狀態,其所需參數都是通過顯式參數提供;一個方法只需要訪問類的靜態域。
靜態方法還有另外一種常見用途,作為工廠方法用以構造對象。之所已使用工廠方法,兩個原因:一是無法命名構造器,因為構造器必須與類名相同;二是當時用構造器時無法改變構造的對象類型。
程式入口 main
方法就是一個典型的靜態方法,其不對任何對象進行操作。在啟動程式時還沒有任何一個對象,靜態的 main
方法將執行並創建程式所需要的對象。每個類都可以有一個 main
方法,作為一個小技巧,我們可以通過這個方法對類進行單元測試。
三、方法參數
Java 中的方法參數總是按值調用,也就是說,方法得到的是所有參數的值的一個拷貝,特別是,方法不能修改傳遞給它的任何參數變數的內容。然而,方法參數有兩種類型:基本數據類型和對象引用。
四、對象構造
如果在構造器中沒有顯式的為域賦值,那麼域會被自動的賦予預設值:數值為 0、布爾之為 false、對象引用為 null。在類沒有提供任何構造器的時候,系統會提供一個預設的構造器。
有些類有多個構造器,這種特征叫做重載(overloading)。如果多個方法有相同的名字、不同的參數,便產生了重載。 Java 中允許重載任何方法,而不僅是構造器方法。要完整的描述一個方法,需要指出方法名以及其參數類型,這個描述被稱作方法的簽名。
通過重載類的構造器方法,可以採用多種形式設置類的實例的初始狀態。當存在多個構造器的時候,也可以在構造器內部通過 this 調用另一個構造器,要註意的是這個調用必須在當前構造器的第一行:
class Test{
Test(int number) {
this(number, (String)number); // 位於當前構造器的第一行
}
Test(int number, String str) {
_number = number;
_string = str;
}
}
初始化塊
在一個類的聲明中,可以包含多個代碼塊。只要構造類的對象,這些塊就會被執行。例如:
class Test{
private int number;
private String name;
/**
* 初始化塊
*/
{
number = 5;
}
Test(){
name = 'Kelsen'
}
public void pring(){
System.out.println(name + "-" + number);
}
}
執行順序為,首先運行初始化塊,然後再運行構造器的主體部分。這種機制不是必須的,也不常見。通常會直接將初始化代碼放在構造器中。
Java 中不支持析構器,它有自動的垃圾回收器,不需要人工進行記憶體回收。但,如果某個資源需要在使用完畢後立刻被關閉,那麼就需要人工來管理。對象用完時可以應用一個 close 方法來完成相應的清理操作。
五、包
藉助於包,可以方便的組織我們的類代碼,並將自己的代碼與別人提供的代碼庫區分管理。標準的 Java 類庫分佈在多個包中,包括 java.lang、java.util 和 java.net 等。標準的 Java 包具有一個層次結構。如同硬碟文件目錄嵌套一樣,也可以使用嵌套層次組織包。所有的標準 Java 包都處於 java
和 javax
包層次中。從編譯器角度看,嵌套的包之間沒有任何關係,每一個都擁有獨立的類集合。
一個類可以使用所屬包中的所有類,以及其他包中的公有類(pbulic class)。 import
語句是一種引用包含在包中的類的簡明描述。package
與 import
語句類似 C++ 中的 namespace
和 using
指令。
import
語句還可以用來導入類的靜態方法和靜態域。
如果要將一個類放入包中,就必須將包的名字放在源文件的開頭,包中定義類的代碼之前。如:
package com.kelsem.learnjava;
public class Test{
// ...
}
如果沒有在源文件中放置 package
語句,這個源文件中的類就被放置在一個預設包中。
包作用域
標記為 private
的部分只能被定義他們的類訪問,標記為 public
的部分可以被任何類訪問;如果沒有指定訪問級別,這個部分(類/方法/變數)可以被同一個包中的所有方法訪問。
類路徑
類存儲在文件系統的目錄中,路徑與包名匹配。另外,類文件也可以存儲在 JAR 文件中。為了使類能夠被多個程式共用,通常把類放到一個目錄中,將 JAR 文件放到一個目錄中,然後設置類路徑。類路徑是所有包含類文件的路徑的集合,設置類路徑時,首選使用 -calsspath 選項設置,不建議通過設置 CLASSPATH 這個環境變數完成該操作。
六、文檔註釋
JDK 包含一個非常有用的工具,叫做 javadoc 。它通過分析我們的代碼文件註釋,自動生成 HTML 文檔。每次修源碼後,通過運行 javadoc 就可以輕鬆更新代碼文檔。Javadoc 功能包括:Javadoc搜索,支持生成HTML5輸出,支持模塊系統中的文檔註釋,以及簡化的Doclet API。詳細使用說明可參考 https://docs.oracle.com/en/java/javase/11/javadoc/javadoc.html
七、類的設計
一定要保證數據私有 務必確保封裝性不被破壞。
一定要對數據初始化 Java 不會對局部變數進行初始化,但會對對象的實例域進行初始化。最好不要依賴於系統預設值,而是顯式的對實例域進行初始化。
不要在類中使用過多的基本類型 通過定義一個新的類,來代替多個相關的基本類型的使用。
不是所有的域都需要獨立的域訪問器和域更改器
將職責過多的類進行分解 如果明顯的可以將一個複雜的類分解為兩個更簡單的類,就應該將其分解。
類名和方法名要能夠體現他們的職責 對於方法名,建議:訪問器以小寫 get
開頭,修改器以小寫 set
開頭;對於類名,建議類名是採用一個名詞(Order)、前面有形容詞修飾的名詞(RushOrder)或動名詞(ing尾碼)修飾名詞(BillingAddress)。
優先使用不可變的類 要儘可能讓類是不可變的,當然,也並不是所有類都應當是不可變的。