跳至主要内容

7.1 Java 組合與繼承語法詳解

組合語法 (Composition syntax)

  • 只需將物件引用放置於新的類別中即可
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
public String toString() { return s; }
}

public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString() {
return
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"source = " + source;
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
} /* Output:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*/
  • 在類別中含有特殊方法 toString()
    • 每個非基本數據類型的物件都有一個 toString() 方法
    • 當編譯器需要一個 String 但你只有一個物件時,編譯器便會調用這個方法
      • SprinklerSystem.toString() 中,表達式 ”source = “ + source 會使編譯器知道你希望將一個String物件(”source =”)與 WaterSource 相加。由於String物件只能與另一個String物件相加,因此編譯器會調用 toString() 方法,將 source 轉換為一個String
  • 編譯器不會為每個引用都建立預設對象,因為這樣會造成不必要的負擔。要初始化這些引用,可以在以下幾個位置進行:
    1. 在定義物件的地方,意味著他們總是能夠在 constructor 被調用前初始化
    2. 在 constructor 中
    3. 正在使用物件之前,稱為惰性初始化(lazy initialization),在生成物件不值得且不必每次都生成的情況下可以減少負擔
    4. 使用實例初始化(instance initialization)
class Soap {
private String s;
Soap() {
print("Soap()");
s = "Constructed";
}
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() {
print("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// Instance initialization:
{ i = 47; }
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
*/

繼承語法 (Inheritance syntax)

  • 當建立一個類別時,總是會進行繼承,除非明確指定從其他類別繼承,否則會隱式地從 Java 的標準根類別 Object 繼承
  • 組合的語法比較直觀,而繼承則使用特定的語法。在繼承的過程中,需要先聲明‘新類別與舊類別相似’,這可以透過在 class body 的左大括號前加上 extends 關鍵字和 base class 來實現
  • 繼承後,新類別會自動擁有基類中的所有字段和方法
class Cleanser {
private String s = "Cleanser";
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public String toString() { return s; }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
print(x);
}
}

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();
print(x);
print("Testing base class:");
Cleanser.main(args);
}
} /* Output:
Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()
*/
  • scrub() 方法中所見,使用基類定義的方法進行修改是可行的
  • scrub() 方法中不能直接調用 scrub(),否則會產生無限遞迴,這不是我們所期望的。為了解決此問題,Java 用 super 關鍵字來表示超類(superclass),即當前類別繼承自的類別。在這裡,super.scrub() 表示調用基類版本的 scrub() 方法

初始化 base class

  • 正確初始化基類只有一種方法:在子類的 constructor 中調用基類的 constructor 來執行初始化
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
*/
  • 可以發現建立過程是從基類向外擴展的,基類在衍生類別的 constructor 可以訪問它之前就已經完成初始化

帶參數的 constructor

  • 上面的例子中,所有的 constructor 都是無參數的,編譯器可以簡單地調用它們,而不需要考慮要帶什麼參數
  • 如果沒有預設的 constructor,或是需要調用帶參數的基類 constructor,就需要使用關鍵字 super 顯式地調用基類的 constructor 並傳遞適當的參數
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}

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

public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} /* Output:
Game constructor
BoardGame constructor
Chess constructor
*/

委派模式 (Delegation)

  • Java 在類別複用上的第三種關係稱為委派,但 Java 並沒有提供直接的語法支援
  • 委派是一種介於繼承和組合之間的折衷方法。它將一個對象放置於新建的類別中,同時在新的類別中暴露出成員物件的所有方法(如同繼承)
public 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 SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) { this.name = name; }
public String toString() { return name; }
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}
  • 在此範例中 SpaceShip 並非真正的 SpaceShipControls 類型,即使你可以”告訴” SpaceShip 向前 (forward()),更精確來說應該是 SpaceShip 包含 SpaceShipControls,但同時 SpaceShip 又需要將所有的 SpaceShipControls 的方法暴露出來,而委派解決了這一難題
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);
}
}
  • 如此一來其接口便跟使用繼承得到的相同
  • 使用委派可以擁有更多的控制力,因為可以只選擇提供物件方法的子集

結合使用組合跟繼承

class Plate {
Plate(int i) {
print("Plate constructor");
}
}

class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
print("DinnerPlate constructor");
}
}

class Utensil {
Utensil(int i) {
print("Utensil constructor");
}
}

class Spoon extends Utensil {
Spoon(int i) {
super(i);
print("Spoon constructor");
}
}

class Fork extends Utensil {
Fork(int i) {
super(i);
print("Fork constructor");
}
}

class Knife extends Utensil {
Knife(int i) {
super(i);
print("Knife constructor");
}
}

// A cultural way of doing something:
class Custom {
Custom(int i) {
print("Custom constructor");
}
}

public class PlaceSetting extends Custom {
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
print("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
} /* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*/
  • 雖然編譯器強制初始化 base class,但並不監督成員物件的初始化,在使用上必須注意

名稱遮蔽 (Name hiding)

  • 如果 Java 的基類擁有某個被多次多載(overloading)的方法名稱,在派生類別中重新定義該方法名稱並不會隱藏基類中的任何版本(與C++不同)
  • 無論是在衍生類別還是基類中對方法進行定義,多載都會正常運作
class Homer {
char doh(char c) {
print("doh(char)");
return 'd';
}
float doh(float f) {
print("doh(float)");
return 1.0f;
}
}

class Milhouse {}

class Bart extends Homer {
void doh(Milhouse m) {
print("doh(Milhouse)");
}
}

public class Hide {
public static void main(String[] args) {
Bart b = new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
} /* Output:
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*/
  • 此例中,雖然 Bart 定義了新的多載方法(C++需要屏蔽基類方法),但 BartHomer 的所有多載方法都是可用的
  • 使用與基類完全相同的簽名和回傳類型來覆蓋具有相同名稱的方法可能會令人迷惑不解(C++為了防止錯誤,直接禁止這麼做)
  • Java SE5 新增了 @Override 註解(非關鍵字,但可以當關鍵字使用),當想要覆蓋某個方法時可以添加這個註解。如果沒有注意到重載而嘗試覆蓋方法,編譯器會生成錯誤訊息
// {CompileTimeError} (Won't compile)
// Message: method does not override a method from its superclass

class Lisa extends Homer {
@Override void doh(Milhouse m) {
System.out.println("doh(Milhouse)");
}
}

在組合與繼承之間選擇

  • 組合通常用於在新類別中使用現有類別的功能而非它的介面,即在新類別中嵌入某個物件,讓其實現所需的功能
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);
}
}
  • 將成員設為 public 有助於使用者了解如何使用此類別,降低開發者所面臨的程式複雜度。但除非在這種情況下,一般應將字段設為 private
  • 繼承是使用某個現有的類別,並開發它的一個特殊版本
    • 意味著使用一個通用類別,並根據特殊需求將其特殊化
  • “is-a” 的關係用繼承來表達,而 “has-a”