一.類複用的兩種方式:
組合(composition)--新類由已有類的對象組成。複用已有類的功能,而不是形式。
繼承(inheritance)--以已有類的類型(type)建立新類。無需改變已有類,隻是采用新類的形式并添加所需代碼。
二.toString()方法的顯式、隐式調用(對象作為String來使用時)。
三.null引用通路時報異常,但列印時不受影響。
四.lazy initialization:在用到時才初始化。
五.繼承:顯式繼承其它類,隐式繼承Object類。extends關鍵字
六.為了測試類,可以在每個類中添加一個main方法,而且完成類時也不需要删除。
任何類(public、package access)的public main方法都可以調用,不論是通過指令行java+類名,還是顯式的調用classname.main()。
七.為了繼承,一般原則:所有fields設為private,methods為public。(隻是個基本的思想問題,protected、private當然也可以用)。
八.super關鍵字,用來通路父類。
九.派生類對象裡包含一個基類的對象(subobject)。
十.Java自動在派生類構造器中插入調用基類構造器的代碼,即使未定義派生類構造器,編譯器仍會産生一個預設構造器來調用基類構造器,而且基類構造器調用發生在派生類構造器之前。
那麼,是不是可以認為基類構造器調用是派生類構造器的第一條語句,進而在派生類構造器調用的時候才調用呢?不是。請看這個代碼:
import static net.mindview.util.Print.*;
class A {
A() { print("A constructor"); }
}
class B {
B() { print("B constructor"); }
}
public class C extends A {
B b = new B();
public static void main(String[] args) {
C c = new C();
}
}
按照初始化順序,那麼定義時的初始化發生在構造器之前,也就是b的指派應該發生在C的預設構造器之前,如果基類構造器調用是派生類構造器的第一條語句,進而在派生類構造器調用的時候才調用,那運作結果應該是
B constructor
A constructor
而實際結果是
A constructor
B constructor
顯然,"基類構造器調用是派生類構造器的第一條語句,進而在派生類構造器調用的時候才調用"這個說法是錯誤的。應該是:基類的初始化發生在派生類的初始化(包括自動初始化、構造器等等)之前。
前面說的是無參構造器(預設構造器),對于有參數的構造器:
1. 如果基類構造器都有參數,必須顯式調用基類構造器(super(...))(如果不,編譯器報錯,找不到基類預設構造器),而且必須是派生類構造器的第一條語句(如果不,編譯器報錯,找不到基類預設構造器和super調用必須是第一條語句兩個錯誤)。
2. 如果基類構造器沒有參數,則不需顯式調用,編譯器會為你完成。
3. 如果派生類有多個構造器,在基類構造器有參數的時候,你顯然得在所有派生類構造器中加入super(...);如果基類構造器沒有參數,編譯器會在所有派生類構造器内加入對基類構造器的調用(因為編譯器不知道你會調用哪個構造器,對吧?嘿嘿)。 看例子
import static net.mindview.util.Print.*;
class A {
A() { print("A constructor"); }
}
class B {
B(int i) { print("B constructor"); }
}
public class C7 extends A {
B b = new B(1);
public C7(int i)
{
// super(i);
b = new B(i);
print("C(int i) constructor");
}
public C7()
{
print("C() constructor");
}
public static void main(String[] args) {
C7 c = new C7(10);
c = new C7();
}
}
結果:
A constructor
B constructor
B constructor
C(int i) constructor
A constructor
B constructor
C() constructor
在顯式調用的情況下,似乎上面的讨論又是錯的,基類構造器調用就是派生類構造器的第一條語句,在派生類構造器調用的時候才調用。結果呢?再次證明這句話是錯的。你仍然得說:基類的初始化發生在派生類的初始化(包括自動初始化、構造器等等)之前 。看這個例子:
import static net.mindview.util.Print.*;
class A {
A(int i) { print("A constructor"); }
}
class B {
B(int i) { print("B constructor"); }
}
public class C7 extends A {
B b = new B(1);
public C7(int i)
{
super(i);
b = new B(i);
print("C constructor");
}
public static void main(String[] args) {
C7 c = new C7(10);
}
}
A constructor
B constructor
B constructor
C constructor
結果說明了一切,雖然你super(i)是在派生類構造器的第一條語句。
總之一句話,未顯式調用基類構造器的情況下,編譯器就查找基類的預設構造器,找不到報錯;找到,加入對它的調用。基類沒有預設構造器,就必須顯式調用。基類的初始化發生在派生類的初始化(包括自動初始化、構造器等等)之前。
十一. 委托(delegation):Java不直接支援。
delegation介于組合和繼承之間,新類包含一個成員對象(composition),但同時又在新類中公開該對象的所有(或部分)方法(象繼承)。用于處理不能簡單的用"has-a"、"is-a"或"is-like-a"來描述的類互相關系的問題(如太空梭不是"is-a"或"is-like-a"太空梭控制器,但又必須能夠進行前進、後退等控制器的操作)。
可以選擇提供部分對象成員的方法,使得應用代理更靈活。
java語言并不支援這種機制,但IDE通常都支援(如JetBrains Idea IDE等)。
delegation和繼承的不同 ,參見DetergentDelegation和Detergent兩個類(練習11和書中的源代碼):方法執行方式不同(繼承通過調用繼承父類的方法(不需顯式調用),delegation調用自己的方法,其中是對對象成員的方法調用),結果不同。
十二. 編譯器并不監督對成員對象的初始化,是以要特别注意。
十三. Java沒有析構器,因為我們習慣忘記對象而不是銷毀對象。
十四. 確定正确清除。
一般情況,垃圾回收器就夠了。
如果必須及時清理某些資源,就必須寫一個特殊的方法,而且應當讓使用者知道必須調用該方法。
首要任務是必須把清除動作置于finally語句中,預防異常的發生?
清理應按照與初始化相反的次序進行,因為一個子對象可能依賴于另一個,例如繼承類可能依賴于基類的對象(繼承析構可能要用到基類的對象)。基本假設是後建立的依靠先建立的,是以要先釋放後建立的。 (比如你建立了一個按鈕,放到一個panel上,panel先建立,按鈕後建立,釋放時肯定先釋放按鈕,後釋放panel)。
十五. try and finally:不管怎麼退出try語句(正常或異常),finally的語句始終會執行。
十六. 垃圾回收器可能以任何順序回收對象,有時需要按照順序,是以不能依賴。(還有一個原因,就是垃圾回收器不知道什麼時候工作,而且可能永遠不會被調用。)
十七. 重載可以跨越基類和派生類,在派生類定義的重載方法不會隐藏任何基類的同名方法(與C++不同,C++會隐藏);當然可以在派生類定義與基類方法簽名完全相同(包括傳回值)的方法實作重寫(override)。這又常常會令人迷惑不解(這也是C++不允許這麼做的原因)。
Java SE5引入了@override注釋,不是關鍵字,用來說明你的方法是override而不是overload,這樣編譯器在你的實作是overload(而不是你需要的override)時,可以給出錯誤資訊。
十八. 組合和繼承的選擇
最根本的:"has-a"和"is-a"。如Car is a vehicle,繼承;Car has a Engine,組合。
組合用于新類需要已有類的功能,而不是其接口時。即新類借助已有類對象實作某種功能,但使用者看到的接口是新類的接口,而不是所包含對象的接口。(例如,可能需要某種資料結構List、Set等來存儲對象,借助它們的特性實作類的功能,但類的接口并不是它們的接口。)
一般情況,組合中對象應該為private。 但也有少數情況,直接通路類成員對象更好,此時可為public(所包含對象本身實作也是隐藏的,是以沒有安全問題)。這樣有個好處是可以幫助用戶端程式員了解接口,使類更易用,而且對類的創造者來說,減少了所需的代碼複雜度(如Car的例子,參見例Car)。但絕大多數情況還是應該為private。
雖然繼承對OOP來說很重要,當使用更多的是組合而不是繼承。應當保持謹慎的态度,隻有在确信需要繼承的時候才使用。最清晰的辦法就是仔細考慮,"是不是真的需要upcasting?"如果是真的,就用繼承。這也是選擇使用組合還是繼承的一個好辦法。
十九. 最好的方法是讓資料成員(fields)為private(而不是protected),而通過protected methods來允許類繼承者通路。
二十. upcasting:安全,繼承類是基類的超集,至少包含基類的方法。upcasting隻會丢失方法,而不是擷取方法(轉型之後對所有方法的調用都是安全的)。downcasting則需要類型檢查。
二十一. final關鍵字:這是無法改變的。兩個理由:設計或效率。
二十三. final資料:常量-永恒不變的編譯時常量(compile-time constant)和不希望被改變的運作時初始化的資料。
編譯時常量,必須是基本類型 ,而且用final定義(public static final定義更常用) ,隻能在定義時指派(隻有在定義時指派的才是編譯期常量),大寫,下劃線分隔。 編譯器可把它用于任何用到它的表達式中,表達式可以在編譯時計算,減少運作期負擔。編譯期常量可能會因為優化而消失,而且編譯器處理所有編譯期常量沒有太大差別(編譯器隻是簡單的用常量代替用到它的地方)。但運作時初始化的常量是有差別的, 例如static和non-static的差別。
final對象,引用是常量,不可變(即初始化後不可再指向另一個對象),但引用所指的對象内容是可變的。注意,Java未提供使任何對象永恒不變的途徑(你可以自己實作這樣的類),包括數組(即不能定義常量數組)。final引用并沒有final基本類型用處大(有用final引用的必要麼?它所指向的對象内容是可變的)。
static final常量,大寫,下劃線分隔。
final定義,并不代表其值是編譯期就決定的。
關于static final和final定義的編譯期常量,得多說兩句。static不用new對象就可以用,而non-static則必須要建立一個對象之後才可以,這是必須的。查詢byte code後,發現static final定義的,并不在類中加入相應的field,在用到它的地方用常量代替;而final定義的,在類中是有field,雖然用到它的地方仍然是用常量代替(還有就是可以blank final,見後面)。 這個代碼
class AmazingCls {
final static double c = Math.random(); //(1)
//static int a = 18; //(2)
final static int a =18; //(3)
final int b = 19;
static{
System.out.println("-----static---------");
}
{
System.out.println("-----instance-------");
}
public AmazingCls(){
System.out.println("------對象被建立---");
}
public static void main(String[] args){
// int b = AmazingCls.a;
System.out.println("------main()--------");
}
}
其byte code反彙編
static final double c;
static final int a;
final int b;
public AmazingCls();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 19
7: putfield #2; //Field b:I
10: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #4; //String -----instance-------
15: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;
21: ldc #6; //String ------對象被建立---
23: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: return
public static void main(java.lang.String[]);
Code:
0: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7; //String ------main()--------
5: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
static {};
Code:
0: invokestatic #8; //Method java/lang/Math.random:()D
3: putstatic #9; //Field c:D
6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #10; //String -----static---------
11: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: return
其中a沒有了。。。
二十四. 空白final(blank finals):final定義,但未賦初始值。必須在使用前初始化(在每個構造器中,編譯器會保證-不初始化,報錯?)。好處是:每個對象的final field可以不同,但又都不可變。
二十五. final參數:在方法内不能改變final參數引用(不能使它指向新的對象);final基本類型,隻讀,主要用于傳資料給内部類。
二十六. final方法:禁止override,出于設計考慮。另外,final方法效率更高(早期JVM)。
Java早期實作,final方法調用以内聯(inline)的形式進行,消除了方法調用的開銷(overhead)。對于大的方法,效果不明顯。
最近的JVM(尤其是HotSpot技術)能自動檢測這些情況并進行優化。是以,Java SE5/6中,應該把效率問題交給JVM來處理,而隻是在需要禁止override時才使用final方法。
不要陷入強調提前優化的陷阱,如果程式執行慢,并不一定能通過final來解決。程式加速,請參見http://MindView.net/Books/BetterJava profiling。
private方法是final方法(隐式),不可override,是以不需要額外加上final。可以在派生類中定義同名的方法,但不是override,隻是定義了一個同名的新方法而已。此時,向上轉型後,調用該方法會報錯,因為是private,即使派生類中的同名方法是public。 對于override方法,加上@Override,可以解決一些問題(如果不允許override,會報錯)。
隻有類接口才可以繼承(非final)。final public or protected方法,嘗試override都會報錯(即不可在派生類中定義相同簽名的方法,不同簽名?OK,of course,重載而已)。
二十七. final類:不可繼承,其方法也是隐式final;fields可以為final,也可以不是,不受影響。
二十八. 慎用final禁止繼承或者override。無法預知類複用的方式,尤其是通用類。
二十九. 類加載發生在第一次使用的時候(建立對象,對static field或static方法的通路)。這同時也是static初始化的時候,隻加載一次。
注意,類的構造器也是隐式static的。是以可以簡單的說:類在第一次對其static成員(field and method)進行通路的時候加載。
三十. 有繼承時的初始化順序(加載派生類時,基類也同時加載):
1. 父類靜态成員初始化、顯式靜态初始化
2. 子類靜态成員初始化、顯式靜态初始化
3. 父類自動初始化、子類自動初始化(簡單的對象記憶體清0)
4. 父類執行個體初始化(specifying initialization and instance clause)
5. 父類構造器
6. 子類執行個體初始化(specifying initialization and instance clause)
7. 子類構造器
三十一. 程式開發式一個增量過程(incremental development)