天天看點

Java 程式設計思想 -- 類再生(七)

類再生

Think in Java 第六章 類再生,持續更新中,subscribe (。→‿←。) 醬

“Java引人注目的一項特性是代碼的重複使用或者再生。但最具革命意義的是,除代碼的複制和修改以外,我們還能做多得多的其他事情。”

在新類裡簡單地建立原有類的對象 new Car()。我們把這種方法叫作“合成”,因為新類由現有類的對象合并而成。

它建立一個新類,将其作為現有類的一個“類型”。我們可以原樣采取現有類的形式,并在其中加入新代碼,同時不會對現有的類産生影響。這種魔術般的行為叫作“繼承”(Inheritance)

合成的文法

為進行合成,我們隻需在新類裡簡單地置入對象句柄即可

//: SprinklerSystem.java
// Composition for code reuse
package c06;

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = new String("Constructed");
  }
  public String toString() { return s; }
}

public class SprinklerSystem {
  private String valve1, valve2, valve3, valve4;
  WaterSource source; //對象句柄會初始化成null
  int i;
  float f;
  void print() {
    System.out.println("valve1 = " + valve1);
    System.out.println("valve2 = " + valve2);
    System.out.println("valve3 = " + valve3);
    System.out.println("valve4 = " + valve4);
    System.out.println("i = " + i);
    System.out.println("f = " + f);
    System.out.println("source = " + source);
  }
  public static void main(String[] args) {
    SprinklerSystem x = new SprinklerSystem();
    x.print();
  }
} ///:~
           

希望句柄得到初始化,可在下面這些地方進行:

(1) 在對象定義的時候。這意味着它們在建構器調用之前肯定能得到初始化。

(2) 在那個類的建構器中(構造函數中)。

(3) 緊靠在要求實際使用那個對象之前。這樣做可減少不必要的開銷——假如對象并不需要建立的話。

對象句柄初始化 之前 調用 會報 空指針異常(常見的報錯,NullPointException)

繼承的文法

繼承與Java(以及其他OOP語言:Oriented Object Programing)非常緊密地結合在一起。

關鍵字extends

// Detergent.java
// Inheritance syntax & properties

class Cleanser {
  private String s = new String("Cleanser");
  public void append(String a) { s += a; }
  public void dilute() { append(" dilute()"); }
  public void apply() { append(" apply()"); }
  public void scrub() { append(" scrub()"); }
  public void print() { System.out.println(s); }
  public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    x.print();
  }
}

public class Detergent extends Cleanser {
  // Change a method:
  public void scrub() {
    append(" Detergent.scrub()");
    super.scrub(); // Call base-class version
  }
  // Add methods to the interface:
  public void foam() { append(" foam()"); }
  // Test the new class:
  public static void main(String[] args) {
    Detergent x = new Detergent();
    x.dilute();
    x.apply();
    x.scrub();
    x.foam();
    x.print();
    System.out.println("Testing base class:");
    Cleanser.main(args);
  }
} 
           

在Cleanser append()方法裡,字串同一個s連接配接起來。這是用“+=”運算符實作的。同“+”一樣,“+=”被Java用于對字串進行“過載”處理。

進行繼承時,我們并不限于隻能使用基礎類的方法。亦可在衍生出來的類(子類)裡加入自己的新方法。

初始化基礎類

基礎類及衍生類,而不再是以前的一個,是以在想象衍生類的結果對象時,可能會産生一些迷惑。從外部看,似乎新類擁有與基礎類相同的接口,而且可包含一些額外的方法和字段。但繼承并非僅僅簡單地複制基礎類的接口了事。建立衍生類的一個對象時,它在其中包含了基礎類的一個“子對象”。這個子對象(指的是基類對象)就象我們根據基礎類本身建立了它的一個對象。從外部看,基礎類的子對象已封裝到衍生類的對象裡了。

當然,基礎類子對象應該正确地初始化,而且隻有一種方法能保證這一點:在建構器中執行初始化,通過調用基礎類建構器,後者有足夠的能力和權限來執行對基礎類的初始化。在衍生類的建構器中,Java會自動插入對基礎類建構器的調用。下面這個例子向大家展示了對這種三級繼承的應用:

1、編譯器也會為我們自動發出對基礎類建構器(無參)的調用。

class Art {
  Art() {
    System.out.println("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    System.out.println("Drawing constructor");
  }
}
           

2、含有自變量的建構器

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}
           

在衍生類建構器中,對基礎類建構器的調用是必須做的第一件事情, super (i) ; 必須寫在子類構造函數的第一行

確定正确的清除

在C++中,一旦破壞(清除)一個對象,就會自動調用破壞器方法。之是以将其省略,大概是由于在Java中隻需簡單地忘記對象,不需強行破壞它們。垃圾收集器會在必要的時候自動回收記憶體。

垃圾收集器大多數時候都能很好地工作,但在某些情況下,我們的類可能在自己的存在時期采取一些行動,而這些行動要求必須進行明确的清除工作。

我們并不知道垃圾收集器什麼時候才會顯身,或者說不知它何時會調用。是以一旦希望為一個類清除什麼東西,必須寫一個特别的方法,明确、專門地來做這件事情。同時,還要讓客戶程式員知道他們必須調用這個方法。

垃圾收集的順序

不能指望自己能确切知道何時會開始垃圾收集。垃圾收集器可能永遠不會得到調用(記憶體充足不會調)。 c++ 中有析構函數(對象清除前調用)

名字的隐藏

如果Java基礎類有一個方法名被“過載”使用多次,在衍生類裡對那個方法名的重新定義就不會隐藏任何基礎類的版本。

很少會用與基礎類裡完全一緻的簽名和傳回類型來覆寫同名的方法(子類覆寫基類,但是可以通過super.方法調用到基類被覆寫的方法)

到底選擇合成還是繼承

“屬于”關系是用繼承來表達的,而“包含”關系是用合成來表達的。

(類的關系 會在 UML課程 中類圖繪制學習中有更深的了解)

繼承是對一種特殊關系的表達,意味着“這個新類屬于那個舊類的一種類型”。

在面向對象的程式設計中,建立和使用代碼最可能采取的一種做法是:将資料和方法統一封裝到一個類裡,并且使用那個類的對象。有些時候,需通過“合成”技術用現成的類來構造新類。而繼承是最少見的一種做法。

防止繼承的濫用

final關鍵字

聲明“這個東西不能改變”。

final關鍵字的三種應用場合:資料、方法以及類

final資料

常數主要應用于下述兩個方面:

(1) 編譯期常數,它永遠不會改變

(2) 在運作期初始化的一個值,我們不希望它發生變化

對于基本資料類型,final會将值變成一個常數;但對于對象句柄,final會将句柄變成一個常數。進行聲明時,必須将句柄初始化到一個具體的對象。而且永遠不能将句柄變成指向另一個對象。

然而,對象本身是可以修改的。

空白final

允許我們建立“空白final”,它們屬于一些特殊的字段。盡管被聲明成final,但卻未得到一個初始值。無論在哪種情況下,空白final都必須在實際使用前得到正确的初始化。

依然保持其“不變”的本質。

final方法

第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程式時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆寫或改寫,就可以采取這種做法。

final類

類肯定不需要進行任何改變;或者出于安全方面的理由,我們不希望進行子類化(子類處理)。不可被繼承,String 類即是 final類

注意資料成員既可以是final,也可以不是,取決于我們具體選擇。

将類定義成final後,結果隻是禁止進行繼承——沒有更多的限制。然而,由于它禁止了繼承,是以一個final類中的所有方法都預設為final。因為此時再也無法覆寫它們。

可為final類内的一個方法添加final訓示符,但這樣做沒有任何意義。

常用的一個類是Vector。如果我們考慮代碼的執行效率,就會發現隻有不把任何方法設為final,才能使其發揮更大的作用。我們很容易就會想到自己應繼承和覆寫如此有用的一個類,但它的設計者卻否定了我們的想法。但我們至少可以用兩個理由來反駁他們。首先,Stack(堆棧)是從Vector繼承來的,亦即Stack“是”一個Vector,這種說法是不确切的。其次,對于Vector許多重要的方法,如addElement()以及elementAt()等,它們都變成了synchronized(同步的)。

final會造成顯著的性能開銷,可能會把final提供的性能改善抵銷得一幹二淨。

Hashtable(散清單),它是另一個重要的标準類。該類沒有采用任何final方法。

繼承初始化

對整個初始化過程有所認識,其中包括繼承

class Insect {
  int i = 9;
  int j;
  Insect() {
    prt("i = " + i + ", j = " + j);
    j = 39;
  }
  static int x1 = 
    prt("static Insect.x1 initialized");
  static int prt(String s) {
    System.out.println(s);
    return 47;
  }
}

public class Beetle extends Insect {
  int k = prt("Beetle.k initialized");
  Beetle() {
    prt("k = " + k);
    prt("j = " + j);
  }
  static int x2 =
    prt("static Beetle.x2 initialized");
  static int prt(String s) {
    System.out.println(s);
    return 63;
  }
  public static void main(String[] args) {
    prt("Beetle constructor");
    Beetle b = new Beetle();
  }
}
           

在裝載過程中,裝載程式注意它有一個基礎類(即extends關鍵字要表達的意思),是以随之将其載入。無論是否準備生成那個基礎類的一個對象,這個過程都會發生(請試着将對象的建立代碼當作注釋标注出來,自己去證明)。

若基礎類含有另一個基礎類,則另一個基礎類随即也會載入,以此類推。接下來,會在根基礎類(此時是Insect)執行static初始化,再在下一個衍生類執行,以此類推。保證這個順序是非常關鍵的,因為衍生類的初始化可能要依賴于對基礎類成員的正确初始化。

此時,必要的類已全部裝載完畢,是以能夠建立對象。首先,這個對象中的所有基本資料類型都會設成它們的預設值,而将對象句柄設為null。随後會調用基礎類建構器。在這種情況下,調用是自動進行的。但也完全可以用super來自行指定建構器調用(就象在Beetle()建構器中的第一個操作一樣)。基礎類的建構采用與衍生類建構器完全相同的處理過程。基礎順建構器完成以後,執行個體變量會按本來的順序得以初始化。最後,執行建構器剩餘的主體部分。

總結

無論繼承還是合成,我們都可以在現有類型的基礎上建立一個新類型。但在典型情況下,我們通過合成來實作現有類型的“再生”或“重複使用”,将其作為新類型基礎實施過程的一部分使用。但如果想實作接口的“再生”,就應使用繼承。由于衍生或派生出來的類擁有基礎類的接口,是以能夠将其“上溯造型”為基礎類。對于下一章要講述的多形性問題,這一點是至關重要的。

練習

(1) 用預設建構器(空自變量清單)建立兩個類:A和B,令它們自己聲明自己。從A繼承一個名為C的新類,并在C内建立一個成員B。不要為C建立一個建構器。建立類C的一個對象,并觀察結果。

(2) 修改練習1,使A和B都有含有自變量的建構器,則不是采用預設建構器。為C寫一個建構器,并在C的建構器中執行所有初始化工作。

(3) 使用檔案Cartoon.java,将Cartoon類的建構器代碼變成注釋内容标注出去。解釋會發生什麼事情。

(4) 使用檔案Chess.java,将Chess類的建構器代碼作為注釋标注出去。同樣解釋會發生什麼。