天天看點

《JAVA程式設計思想》第四版學習 需要我記住的something –複用類

一.類複用的兩種方式:

      組合(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)