Thinking in Java(7-2) 建構子與多型

Thinking in Java(7-2) 建構子與多型
Photo by orbtal media / Unsplash

在 Java 的物件導向程式設計中,建構子(Constructor)扮演著初始化物件的重要角色。然而,建構子並不具備多型的特性(實際上,建構子是隱式宣告的 static 方法)。在本文中,我們將深入探討建構子與多型的關係,以及它們在物件初始化過程中的行為。

建構子的調用順序

建構子總是在衍生類別的建立過程中被調用,並按照繼承層次逐層向上,確保每個基礎類別的建構子都得到調用。這是必要的,因為只有基礎類別的建構子才擁有適當的知識和權限來初始化其自身的成員。

如果未明確指定基礎類別的建構子,衍生類別將預設調用基礎類別的無參數建構子(預設建構子)。如果基礎類別沒有無參數建構子,編譯器將報錯。

以下是一個示例:

class Meal {
  Meal() { print("Meal()"); }
}

class Bread {
  Bread() { print("Bread()"); }
}

class Cheese {
  Cheese() { print("Cheese()"); }
}

class Lettuce {
  Lettuce() { print("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { print("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { print("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
} /* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*/

初始化順序詳解

  1. 基礎類別建構子調用:從繼承層次的最頂層開始,逐層向下調用基礎類別的建構子。
  2. 成員初始化:按照聲明的順序調用成員的初始化方法。
  3. 衍生類別建構子調用:最後,調用衍生類別自身的建構子。

透過這種方式,可以確保所有物件的成員都被正確初始化,並且基礎類別的初始化先於衍生類別。

繼承與清理

當透過繼承和組合建立新類別時,一般不需要擔心物件的清理問題,因為 Java 的垃圾回收機制會自動處理。然而,當需要進行特定的清理操作時(例如關閉檔案、釋放網路連線等),就必須手動定義清理方法。

如果在衍生類別中需要執行額外的清理操作,應該在衍生類別中覆寫基礎類別的清理方法,並且務必在新方法中調用 super 關鍵字來執行基礎類別的清理,否則基礎類別的清理將不會發生。

以下是一個示例,展示了物件的清理順序:

class Characteristic {
  private String s;
  Characteristic(String s) {
    this.s = s;
    print("Creating Characteristic " + s);
  }
  protected void dispose() {
    print("disposing Characteristic " + s);
  }
}

class Description {
  private String s;
  Description(String s) {
    this.s = s;
    print("Creating Description " + s);
  }
  protected void dispose() {
    print("disposing Description " + s);
  }
}

class LivingCreature {
  private Characteristic p =
    new Characteristic("is alive");
  private Description t =
    new Description("Basic Living Creature");
  LivingCreature() {
    print("LivingCreature()");
  }
  protected void dispose() {
    print("LivingCreature dispose");
    t.dispose();
    p.dispose();
  }
}

class Animal extends LivingCreature {
  private Characteristic p =
    new Characteristic("has heart");
  private Description t =
    new Description("Animal not Vegetable");
  Animal() { print("Animal()"); }
  protected void dispose() {
    print("Animal dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

class Amphibian extends Animal {
  private Characteristic p =
    new Characteristic("can live in water");
  private Description t =
    new Description("Both water and land");
  Amphibian() {
    print("Amphibian()");
  }
  protected void dispose() {
    print("Amphibian dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

public class Frog extends Amphibian {
  private Characteristic p = new Characteristic("Croaks");
  private Description t = new Description("Eats Bugs");
  public Frog() { print("Frog()"); }
  protected void dispose() {
    print("Frog dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    Frog frog = new Frog();
    print("Bye!");
    frog.dispose();
  }
} /* Output:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristic has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive
*/

清理順序詳解

  • 初始化順序:與建構子的調用順序相似,成員物件的初始化順序按照它們的聲明順序,從基礎類別到衍生類別。
  • 清理順序:清理過程的順序與初始化順序相反。首先清理衍生類別中特有的成員,然後逐層向上調用基礎類別的清理方法。

這種順序確保了在清理過程中,基礎類別的成員不會過早被清除,從而避免可能的錯誤。

引用計數的應用

當物件之間存在共享關係時(例如多個物件共享同一資源),需要使用引用計數來追蹤有多少物件正在共享該資源,從而確保在最後一個引用被移除時才真正進行清理。

class Shared {
  private int refcount = 0;
  private static long counter = 0;
  private final long id = counter++;
  public Shared() {
    print("Creating " + this);
  }
  public void addRef() { refcount++; }
  protected void dispose() {
    if(--refcount == 0)
      print("Disposing " + this);
  }
  public String toString() { return "Shared " + id; }
}

class Composing {
  private Shared shared;
  private static long counter = 0;
  private final long id = counter++;
  public Composing(Shared shared) {
    print("Creating " + this);
    this.shared = shared;
    this.shared.addRef();
  }
  protected void dispose() {
    print("disposing " + this);
    shared.dispose();
  }
  public String toString() { return "Composing " + id; }
}

public class ReferenceCounting {
  public static void main(String[] args) {
    Shared shared = new Shared();
    Composing[] composing = { new Composing(shared),
      new Composing(shared), new Composing(shared),
      new Composing(shared), new Composing(shared) };
    for(Composing c : composing)
      c.dispose();
  }
} /* Output:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
*/

在這個例子中,Shared 物件被多個 Composing 物件共享。透過引用計數機制,確保了在最後一個 Composing 物件被清理時,才真正清理 Shared 物件。

建構子內部的多型方法行為

在一般方法中,動態綁定的調用是在運行時決定的。然而,在建構子內部調用動態綁定的方法可能會導致難以預測的結果,因為此時衍生類別的成員可能尚未初始化。

問題示例

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}	

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}	

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

在這個例子中,Glyph 的建構子在調用 draw() 方法時,實際上調用了 RoundGlyph 的覆寫方法。然而,此時 RoundGlyph 的成員變數 radius 尚未初始化,導致輸出為預設值 0,而非預期的值。

初始化過程詳解

  1. 物件記憶體分配並初始化為二進位零:所有基本型別設為預設值,物件引用設為 null
  2. 基礎類別建構子調用:此時可能調用被覆寫的方法,但衍生類別的成員尚未初始化。
  3. 成員初始化:按照聲明順序初始化衍生類別的成員。
  4. 衍生類別建構子調用:執行衍生類別建構子的主體。

解決方案

為了避免在建構子內部調用動態綁定的方法,建議遵循以下準則:

  • 盡可能簡化建構子:在建構子中僅進行基本的初始化操作。
  • 避免在建構子中調用可能被覆寫的方法:這樣可以防止調用到尚未初始化的成員。
  • 如果需要調用方法,確保它們是 finalfinal 方法不會被覆寫,因此調用時是安全的。

總結

建構子在物件的初始化過程中扮演關鍵角色,理解它們的調用順序和行為對於編寫正確、穩定的 Java 程式至關重要。特別是在繼承層次結構中,需要注意建構子與多型的互動,避免在建構子內部調用可能被覆寫的方法。