【Java編程思想】7.復用類
Java 中復用代碼的方法:
- 組合:只需在新的類中產生現有類的對象。
- 繼承:按照現有類的類型來創建新的類,無需改變現有類的形式。
- 代理:繼承和組合之間的方式
7.1 組合語法
其實就是在一個類中引入其他類作為屬性/域。
類中域為基本類型時會被自動初始化為0或 false,對象會被初始化為 null。
初始化對象的引用,可以在代碼中的下列位置中進行:
- 在定義對象的地方-->意味著他們總是能夠在構造器被調用前被初始化
- 在類的構造器中
- 在正要使用這些對象之前(惰性初始化/懶漢式/懶加載)
- 使用實例初始化
7.2 繼承語法
所有的類,都在顯式或隱式的繼承標準根類 Object。
基類中,約定俗成將數據成員都指定為 private,所有方法都指定為 public。
繼承不只是復制基類的接口,當創建了一個導出類的對象時,該對象包含了一個基類的子對象。這個子對象與使用基類直接創建的對象時一樣的,二者區別在於,直接創建的對象來自於外部,而基類的子對象被包裝在導出類對象內部。
在繼承中,初始化對象時,構建的過程是從基類”向外“擴散的,所以基類在導出類構造器可以訪問它之前,就已經完成了初始化。
如果沒有默認的基類構造器,或者需要調用一個帶參數的基類構造器,就必須使用 super 關鍵字顯式地編寫調用基類構造器的語句,並配以適當的參數列表。
7.3 代理
如下
public class SpaceShipControls { void up(int velocity) {} void down(int velocity) {} void turboBoost() {} }
SpaceShipDelegation 實現了 SpaceShipControls 的代理
public class SpaceShipDelegation { private String name; private SpaceShipControls controls = new SpaceShipControls(); public SpaceShipDelegation(String name) { this.name = name; } // Delegated methods: public void down(int velocity) { controls.down(velocity); } public void up(int velocity) { controls.up(velocity); } public void turboBoost() { controls.turboBoost(); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
7.4 結合使用組合和繼承
Java 中沒有 C++ 中析構函數的概念。析構函數是一種在對象被銷毀時可以被自動調用的函數。
有時類需要在其生命周期內執行一些必須的清理活動,這是必須顯式的編寫一個特殊方法,來保證客戶端程序員知道他們必須調用這種方法。因此可以將這一清理動作置於 finally 子句中預防異常的出現。
上面說的這種方法,是適合寫在被繼承的基類中供各種導出類使用。
在清理類的過程中,執行類的所有特定的清理動作,其順序與生成類對象的順序相反(要求基類元素依然存活)。
一個不懂的點?
對於清理這個動作,最好是除了內存以外,不去依賴垃圾回收期做任何事。如果需要進行清理,最好是編寫自己的清理方法,而不是使用finalize()
方法。
如果 Java 的基類擁有某個已被多次重載的方法名稱,那麽在導出類中重新定義該方法名稱,並不會屏蔽其在基類中的任何版本。也就是說,無論在該導出類或者其基類中對方法進行定義,重載機制都會正常工作。
針對上述情況,Java SE5 新增了 @override
註解,當需要覆寫某個方法時,可以選擇添加此註解,用來區別於在導出類重載基類的方法(即同名不同參數的方法)。
@override
註解可以防止在不想重載時而意外的進行重載(這就和 C++ 一致了)。
7.5 在組合和繼承之間選擇
組合和繼承的區別在於:
- 組合:
- 組合顯式的在新的類中放置子對象
- 組合通常用於想在新類中使用現有類的功能而非他的接口的情形,即在新類中嵌入某個對象並讓其實現所需的功能。
- has-a(有一個)關系用組合來表達
- 繼承:
- 繼承隱式的在新的類中放置子對象
- 繼承的時候,使用某個現有類,並會開發一個特殊的版本。這更像是因為需求而去定制的特別實現。
- is-a(是一個)關系用繼承來表達
一個比較清晰的判定方法:
在使用組合或繼承前,仔細考慮是否需要從新類向基類進行向上轉型,如果必須向上轉型,則繼承才是必要的。
7.6 protected 關鍵字
使用 protected 的判定:在使用繼承的時候,如果希望某些成員可以對外部隱藏起來,而仍可以被導出類所訪問。
就是說,對類用戶而言是 private 的,而對繼承於此類或者任何位於同一個包內的類來說是 public 的。
7.7 向上轉型
在繼承的關系中,可以確保基類中的所有方法在導出類中也同樣有效,所以,能夠像基類發送的信息,應該也是同樣也可以向導出類發送的。
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
}
上面的例子中,tune()
可以接受 Instrument
的引用(自身的引用)。而且 Wind.main()
中傳遞給 tune()
方法的參數,是 Wind
的引用。
像這種將 Wind
的引用轉換為 Instrument
的引用的動作,就稱之為向上轉型。
這種命名方式的起源,可能是來自於傳統的 UML 圖中關於類繼承關系的表示。如下:
向上轉型是從一個較專用類型,向較通用類型轉換,因此總是很安全的。
就是說導出類是基類的超集,導出類會具備基類中的方法。因此在向上轉型的過程中,唯一可能發生的事是丟失方法。
Java 中是有向下轉型的,後面會有介紹。
總結來說,就是對象既可以作為他自己本身的類型使用,也可以作為他的基本類型使用,把這種對某個對象的引用視為對其基類類型的引用的做法稱之為向上轉型(因為在 UML 繼承樹的畫法中,基類是在上方的)。
7.8 final 關鍵字
通常關鍵字 final 就是代表著“無法改變”的含義。
對於數據來說,有兩種情況需要使用 final:
- 一個永不改變的編譯時常量(必須是基本數據類型,且在定義時必須進行賦值)。
- 一個在運行時被初始化的值,不希望他被改變。
一個既是 static 又是 final 的域,只占據一段不能被改變的存儲空間。
當對象引用使用 final 時,代表其引用恒定不變,這意味著將無法把它改為指向另一個對象;然而對象自身確是可以被修改的。這種情況也適用於數組,數組也是對象。
final 不意味著某數據可以在編譯時就知道它的值,只能說這個數據在被賦值之後,就再也不能去修改了(基本類型是不能修改值,對象是不能修改引用)。
Java 允許生成“空白 final”(就是聲明為 final 但是為給定初始值的域),但是編譯器會確保空白 final 在使用前一定會被初始化。
對於參數來說,使用 final 以為這無法在方法中更改參數引用所指向的對象。
這一特性主要用來向匿名內部類傳遞數據。
對於方法來說,有兩種情況需要使用 final:
- 需要把方法鎖定,以防止任何繼承類修改它的含義。
- 另一個原因(過去)是因為效率。早起 Java 的實現中,方法指明為 final,就是同意編譯器將針對該方法的調用都轉為內嵌調用。
針對第二個原因,對於早期提高效率的更具體的描述,是說當編譯器發現一個 final 方法調用命令是,會根據自己的判斷,調過插入程序代碼這種正常的方式,而去執行方法調用機制(將參數壓入棧,跳至方法代碼處並執行,然後跳回並清理棧中的參數,處理返回值),並且以方法體重的實際代碼的副本代替方法調用。這樣會消除方法調用的開銷。
這樣當一個方法很大的時候,帶來的程序代碼膨脹,是不會因為內嵌而提高性能的(相當於被內嵌的方法阻塞住了)。
最新的虛擬機的 hotspot 技術可以探測到這種情況,並優化去掉這些效率降低的內嵌調用,所以不需要使用final 方法來進行優化了。
關於 final 和 private
類中的 private 方法其實都隱式地被指定了 final:無法取用 private 的方法是無法被覆蓋的,因此跟被 final 修飾的效果是相同的。
但是當強行覆蓋一個 private 方法時,編譯器是不會報錯的。
這是因為“覆蓋“這種情況只有在某方法是基類接口的一部分的時候才會出現,即,必須能將對象向上轉型為它的基類類型,並調用相同的方法。當基類的某個方法時 private 時,對於外部導出類來說這個方法並不是基類接口的一部分,他只是基類中隱藏的一部分代碼而已。
因此不需要特別在意 private 和 final 的區別,兩者對方法的造成結果都是一致的。
類被 final 修飾時,代表該類不會被繼承。實際上使用的情況:
- 類永遠不需要進行任何改動。
- 處於安全考慮不允許該類被繼承。
被 final 修飾的類中的域依然是可以自定義其是不是 final 的。然而 final 類 中的域和方法其實都是默認是 final 的。。。(這 tm 不是廢話嗎)
7.9 初始化以及類的加載
每個類的編譯代碼,都存在於自己的獨立的文件中,該文件只在需要使用程序代碼是才會被加載。
這通常是指加載發生於創建類的第一個對象只是,但是當訪問 static 域或 static 方法時,也會發生加載(構造器也是 static 方法,某種程度的隱式的,因此更準確的講,類實在其任何 static 成員被訪問時加載的,說的就是你”構造器“)。
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j); // 4
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized"); // 1
static int printInit(String s) {
print(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized"); // 5
public Beetle() {
print("k = " + k); // 6
print("j = " + j); // 7
}
private static int x2 =
printInit("static Beetle.x2 initialized"); // 2
public static void main(String[] args) {
print("Beetle constructor"); // 3
Beetle b = new Beetle();
}
}
1 static Insect.x1 initialized
2 static Beetle.x2 initialized
3 Beetle constructor
4 i = 9, j = 0
5 Beetle.k initialized
6 k = 47
7 j = 39
可以看上面一段程序,運行程序時順序如下:
- 訪問 static 的
Beetle.main()
方法 - 加載器開始啟動,找出 Beetle.class 文件之中的編譯代碼。
- 在進行加載的過程中,編譯器由 extends 關鍵字找到他有基類,於是繼續加載 Insect.class 文件之中的編譯代碼。
- 拋開這個例子,如果基類還有自身的基類,那麽第二個基類會被繼續加載,如此類推向上。
- 根基類中的 static 初始化開始被執行,然後是下一個導出類,如此類推向下。(這種方式確保了帶出類的 static 初始化會依賴於基類成員能夠被正確初始化)
- 至此為止,必要的類已經加載完畢,可以開始創建對象了。
- 對象中所有基本類型被設置為默認值,對象引用設置為 null(通過將對象內存設為二進制零值而生成的)。
- 基類的構造器(其靜態 static 屬性再次暴露)被調用。例子中被自動調用,也可以用 super 來指定對基類構造器的調用。
- 基類構造器和導出類的構造器一樣,以相同的順序經歷相同過程。
- 基類構造器完成後,實例變量按照次序被初始化。
- 構造器其余部分被執行。
【Java編程思想】7.復用類