7、複用

Java 圍繞“類”(Class)來解決問題。我們可以直接使用別人構建或調試過的代碼,而非創建新類、重新開始。

如何在不污染源代碼的前提下使用現存代碼是需要技巧的。在本章裏,你將學習到兩種方式來達到這個目的:

  1. 第一種方式直接了當。在新類中創建現有類的對象。這種方式叫做 “組合”(Composition),通過這種方式複用代碼的功能,而非其形式。
  2. 第二種方式更爲微妙。創建現有類類型的新類。照字面理解:採用現有類形式,又無需在編碼時改動其代碼,這種方式就叫做 “繼承”(Inheritance),編譯器會做大部分的工作。繼承是面向對象編程(OOP)的重要基礎之一。

Java不直接支持的第三種重用關係稱爲委託。這介於繼承和組合之間,因爲你將一個成員對象放在正在構建的類中(比如組合),但同時又在新類中公開來自成員對象的所有方法(比如繼承)

1、組合

僅需要把對象的引用(object references)放置在一個新的類裏,這就使用了組合。

編譯器不會爲每個引用創建一個默認對象,這是有意義的,因爲在許多情況下,這會導致不必要的開銷。初始化引用有四種方法:

  1. 當對象被定義時。這意味着它們總是在調用構造函數之前初始化。
  2. 在該類的構造函數中。
  3. 在實際使用對象之前。這通常稱爲延遲初始化。在對象創建開銷大且不需要每次都創建對象的情況下,它可以減少開銷。
  4. 使用實例初始化。
// reuse/Bath.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Constructor initialization with composition

class Soap {
  private String s;
  Soap() {
    System.out.println("Soap()");
    s = "Constructed";
  }
  @Override
  public String toString() { return s; }
}

public class Bath {
  private String // Initializing at point of definition:在定義時初始化
    s1 = "Happy",
    s2 = "Happy",
    s3, s4;
  private Soap castille;
  private int i;
  private float toy;
  public Bath() {
    System.out.println("Inside Bath()");
    s3 = "Joy";
    toy = 3.14f;
    castille = new Soap();
  }
  // Instance initialization:實例初始化
  { i = 47; }
  @Override
  public String toString() {
    if(s4 == null) // Delayed initialization:延遲初始化
      s4 = "Joy";
    return
      "s1 = " + s1 + "\n" +
      "s2 = " + s2 + "\n" +
      "s3 = " + s3 + "\n" +
      "s4 = " + s4 + "\n" +
      "i = " + i + "\n" +
      "toy = " + toy + "\n" +
      "castille = " + castille;
  }
  public static void main(String[] args) {
    Bath b = new Bath();
    System.out.println(b);
  }
}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/
如果你試圖對未初始化的引用調用方法,則未初始化的引用將產生運行時異常。

2、繼承

在創建類時總是要繼承,因爲除非顯式地繼承其他類,否則就隱式地繼承 Java 的標準根類對象(Object)。使用關鍵字 extends 後跟基類的名稱。當你這樣做時,你將自動獲得基類中的所有字段和方法。

即使程序中有很多類都有 main() 方法,惟一運行的只有在命令行上調用的 main()。

Java的 super 關鍵字引用了當前類繼承的“超類”(基類)

繼承時,你不受限於使用基類的方法。你還可以像向類添加任何方法一樣向派生類添加新方法:只需定義它。

2.1、初始化基類

必須正確初始化基類子對象,而且只有一種方法可以保證這一點 : 通過調用基類構造函數在構造函數中執行初始化,該構造函數具有執行基類初始化所需的所有適當信息和特權。Java 自動在派生類構造函數中插入對基類構造函數的調用。

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

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

public class Cartoon extends Drawing {
  public Cartoon() {
    System.out.println("Cartoon constructor");
  }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
  }
}
/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/

2.2、帶參構造函數

如果沒有無參數的基類構造函數(當你編寫了無參構造函數時,編譯器不會自動爲你創建無參構造函數),或者必須調用具有參數的基類構造函數,則必須使用 super 關鍵字和適當的參數列表顯式地編寫對基類構造函數的調用

3、委託

 class SpaceShipControls {
  void up(int velocity) {}
  void down(int velocity) {}
  void left(int velocity) {}
  void right(int velocity) {}
  void forward(int velocity) {}
  void back(int velocity) {}
  void turboBoost() {}
}
public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls =
    new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  // Delegated methods:委託方法
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  }
  public static void main(String[] args) {
    SpaceShipDelegation protector =
      new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}

4、結合組合與繼承

儘管編譯器強制你初始化基類,並要求你在構造函數的開頭就初始化基類,但它並不監視你以確保你初始化了成員對象。注意類是如何幹淨地分離的。你甚至不需要方法重用代碼的源代碼。你最多隻導入一個包。(這對於繼承和組合都是正確的。)

4.1、保持適當的清理

始化和清理章節提到,你無法知道垃圾收集器何時會被調用,甚至它是否會被調用。因此,如果你想爲類清理一些東西,必須顯式地編寫一個特殊的方法來完成它,並確保客戶端程序員知道他們必須調用這個方法。最重要的是——正如在"異常"章節中描述的——你必須通過在 *finally *子句中放置此類清理來防止異常。

class Shape {
  Shape(int i) {
    System.out.println("Shape constructor");
  }
  void dispose() {
    System.out.println("Shape dispose");
  }
}

class Circle extends Shape {
  Circle(int i) {
    super(i);
    System.out.println("Drawing Circle");
  }
  @Override
  void dispose() {
    System.out.println("Erasing Circle");
    super.dispose();
  }
}

class Triangle extends Shape {
  Triangle(int i) {
    super(i);
    System.out.println("Drawing Triangle");
  }
  @Override
  void dispose() {
    System.out.println("Erasing Triangle");
    super.dispose();
  }
}

class Line extends Shape {
  private int start, end;
  Line(int start, int end) {
    super(start);
    this.start = start;
    this.end = end;
    System.out.println(
      "Drawing Line: " + start + ", " + end);
  }
  @Override
  void dispose() {
    System.out.println(
      "Erasing Line: " + start + ", " + end);
    super.dispose();
  }
}

public class CADSystem extends Shape {
  private Circle c;
  private Triangle t;
  private Line[] lines = new Line[3];
  public CADSystem(int i) {
    super(i + 1);
    for(int j = 0; j < lines.length; j++)
      lines[j] = new Line(j, j*j);
    c = new Circle(1);
    t = new Triangle(1);
    System.out.println("Combined constructor");
  }
  @Override
  public void dispose() {
    System.out.println("CADSystem.dispose()");
    // The order of cleanup is the reverse
    // of the order of initialization:
    t.dispose();
    c.dispose();
    for(int i = lines.length - 1; i >= 0; i--)
      lines[i].dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    CADSystem x = new CADSystem(47);
    try {
      // Code and exception handling...
    } finally {
      x.dispose();
    }
  }
}
/* Output:
Shape constructor
Shape constructor
Drawing Line: 0, 0
Shape constructor
Drawing Line: 1, 1
Shape constructor
Drawing Line: 2, 4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem.dispose()
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line: 2, 4
Shape dispose
Erasing Line: 1, 1
Shape dispose
Erasing Line: 0, 0
Shape dispose
Shape dispose
*/


還必須注意基類和成員對象清理方法的調用順序,以防一個子對象依賴於另一個子對象。首先,按與創建的相反順序執行特定於類的所有清理工作。(一般來說,這要求基類元素仍然是可訪問的。) 然後調用基類清理方法。

除了內存回收外,你不能依賴垃圾收集來做任何事情。如果希望進行清理,可以使用自己的清理方法,不要使用 finalize()。

4.2、名稱隱藏

如果 Java 基類的方法名多次重載,則在派生類中重新定義該方法名不會隱藏任何基類版本。不管方法是在這個級別定義的,還是在基類中定義的,重載都會起作用。

5、組合與繼承的選擇

  • 當你想在新類中包含一個已有類的功能時,使用組合,而非繼承。也就是說,在新類中嵌入一個對象(通常是私有的),以實現其功能。新類的使用者看到的是你所定義的新類的接口,而非嵌入對象的接口。

有時讓類的用戶直接訪問到新類中的組合成分是有意義的。只需將成員對象聲明爲 public 即可(可以把這當作“半委託”的一種)。

// reuse/Car.java
// Composition with public objects
class Engine {
    public void start() {}
    public void rev() {}
    public void stop() {}
}

class Wheel {
    public void inflate(int psi) {}
}

class Window {
    public void rollup() {}
    public void rolldown() {}
}

class Door {
    public Window window = new Window();

    public void open() {}
    public void close() {}
}

public class Car {
    public Engine engine = new Engine();
    public Wheel[] wheel = new Wheel[4];
    public Door left = new Door(), right = new Door(); // 2-door

    public Car() {
        for (int i = 0; i < 4; i++) {
            wheel[i] = new Wheel();
        }
    }

    public static void main(String[] args) {
        Car car = new Car();
        car.left.window.rollup();
        car.wheel[0].inflate(72);
    }
}

因爲在這個例子中 car 的組合也是問題分析的一部分(不是底層設計的部分),所以聲明成員爲 public 有助於客戶端程序員理解如何使用類,且降低了類創建者面臨的代碼複雜度。但是,記住這是一個特例。通常來說,屬性還是應該聲明爲 private。

  • 當使用繼承時,使用一個現有類並開發出它的新版本。通常這意味着使用一個通用類,併爲了某個特殊需求將其特殊化。

“是一個”的關係是用繼承來表達的,而“有一個“的關係則用組合來表達。

6、protected

想把一個事物儘量對外界隱藏,而允許派生類的成員訪問。

關鍵字 protected 就起這個作用。它表示“就類的用戶而言,這是 private 的。但對於任何繼承它的子類或在同一包中的類,它是可訪問的。”(protected 也提供了包訪問權限)

儘管可以創建 protected 屬性,但是最好的方式是將屬性聲明爲 private 以一直保留更改底層實現的權利。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章