實現不可變類如何禁止子類化?

monkeysayhi發表於2018-11-12

實現不可變類時要求禁止子類化。本文先講禁止子類化的方式,最後解釋為什麼要禁止子類化。

如何禁止子類化

常用姿勢

最簡單的手段是將類宣告為final,如String、Integer等常用的值類。但這樣缺乏靈活性:不僅禁止了使用者的子類化,開發者也無法利用子類化減少編碼工作。

儘管這種手段完全沒有變通,卻是我們使用最多的一種。只有你需要上述靈活性的時候,再去考慮下述方式。

不常用但你需要掌握的姿勢

還有一種不常用,但更靈活的方法:靜態工廠方法+私有構造器。

完全禁止子類化(效果類似於final修飾)

如果希望Parent3完全不可子類化,除了用final修飾Parent3以外,還可以用private修飾其所有構造方法,這樣Child3因無法呼叫父類的構造方法,也無法通過編譯:

public class Parent3 {
  private Parent3() {
  }
}

// 無法呼叫父類的構造方法,因此無法通過編譯,即Parent3無法子類化
public class Child3 extends Parent3 {
  private Child3() {
    super();
  }
}複製程式碼

通過靜態工廠方法構造Parent3的例項。

更靈活的子類化限制

但是,如果放鬆Parent3構造方法的訪問許可權, 我們還能得到更靈活的子類化限制。比如允許包級私有的子類化:

public class Parent5 {
  Parent5() {
  }
}

// 只要Child5與Parent5定義在同一個包內,就可以子類化
public class Child5 extends Parent5 {
  Child5() {
    super();
  }
}複製程式碼

需要注意的是,Java的覆寫機制要求覆寫方法(Child5())的許可權不低於被覆寫方法(Parent5())。這造成了一種危險:如果將Child5()宣告為public,那麼Child5變得可子類化,間接實現了Parent5的子類化

PS:以上實現不能定義為內部類,如果有疑問,你需要回憶private的語義。

為什麼要禁止子類化

如果允許子類化,在發生多型的情況下,通過覆寫子類的訪問器,可以讓子類冒充父類,讓父類“看起來”是可變的:

  public class ImmutableParent {
    private final int imVal;

    public ImmutableParent(int imVal) {
      this.imVal = imVal;
    }

    public int getImVal() {
      return imVal;
    }
  }

  …

  public class MutableChild extends ImmutableParent {
    private int mVal;

    public MutableChild(int imVal) {
      super(imVal);
      mVal = imVal;
    }

    // 覆寫父類的不可變欄位 imVal 的訪問器, 發生多型時子類例項就能偽裝成父類例項, 讓使用者訪問可變欄位 mVal
    @Override
    public int getImVal() {
      return mVal;
    }

    // 而偽裝者撕下面具時(改用子類引用), 就能隨意修改可變欄位 mVal
    public void setImVal(int mVal) {
      this.mVal = mVal;
    }
  }複製程式碼

總結

上述方式從語法層面保證了不可變類的禁止子類化。儘管我們能通過其他辦法在多型訪問時判斷當前物件是父類還是子類的例項,但哪種方式更恰當呢?顯然是前者,兩個理由:

  1. 用合適的方式做合適的事
  2. 語法優於約定

本文連結:實現不可變類時如何禁止子類化?
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章