The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目17:継承のための設計および文書化する、でなければ継承を禁止する

継承させることを意図するクラスを作成するときは、継承のために設計し、文書化する。 そうでない場合には継承させないようにする。

自己利用(self-use)の文書化

クラスはオーバーライド可能なメソッドの自己利用を文書化する必要がある。 個々の public や protected のメソッドコンストラクタに対して、どのようなオーバーライド可能なメソッドをどのような順番で呼び出して、どのように影響するかを文書化するべきである。

ここで「オーバーライド可能」とは、final ではなく、public か protected であることを意味している。

慣例としてオーバーライド可能なメソッドはドキュメントに「この実装は(This implementation)」という記述を含む。例えば、java.util.AbstractCollection のドキュメントには、以下のように記されている。

public boolean remove(Object o)

(省略)

この実装は、指定された要素を探すのにコレクションをイテレートします。もし、その要素が見つかれば、イテレータの remove メソッドを利用して、コレクションからその要素を取り除きます。本コレクションの iterator メソッドから返されたイテレータが remove を実装していなければ、この実装は UnsupportedOperationException をスローすることに注意してください。

この文書は、iterator メソッドをオーバーライドすることで、remove メソッドにどのように影響するかを示している。

本来、良い API ドキュメントは何をするのかを記述し、どのように行うかは記述するべきではない。これは、継承によってカプセル化が破壊されていることを示している。クラスが安全にサブクラス化されるためには、実装の詳細を記述する必要があるのだ。

継承のためのテスト

継承のために設計・文書化したクラスをテストする唯一の方法は、実際にサブクラスを書くこと。大抵、3つくらいのサブクラスを書けば十分で、2つくらいは他の開発者に書いてもらうのが理想である。

継承を可能にするためには、コンストラクタはオーバーライド可能なメソッドを呼び出してはならない。

スーパークラスコンストラクタは、サブクラスのコンストラクタより前で実行される。 もし、オーバーライドしているメソッドが、サブクラスのコンストラクタ内の初期化に依存していると意図通りに動作しなくなる。

public class Super {
    public Super() {
        overrideMe();
    }
    public void overrideMe() {
    }
}

public final class Sub extends Super {
    private final Date date;
    Sub() {
        date = new Date();
    }

    @Override
    public void overrideMe() {
        System.out.println(date);
    }
}

public class Main {
    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

上記の Main を実行した結果、以下のようになる。

null
Sat Aug 29 23:38:42 JST 2015

一行目は、スーパークラスコンストラクタ内で、サブクラスのコンストラクタで date が初期化される前にスーパークラスの overrideMe が呼び出されているため null が表示される。

Cloneable と Serializable

継承のために設計しているクラスでは、一般的に Cloneable と Serializable は実装するべきではない。 この二つのどちからのインタフェースを実装すると、クラスを継承するユーザにかなりの実装の負担をかける。

特に、clone と readObject 内でオーバーライド可能なメソッドは呼び出してはならない。コンストラクタと同様に、サブクラスで初期化されていない状態で、サブクラスでオーバーライドされたメソッドが呼び出される可能性がある。

また、Serializable を実装すると決めたら、readResolve メソッド、writeReplace メソッドは private ではなく protected にしなければならない。 private の場合サブクラスによって黙って無視され、意図しない挙動となる。

クラスを継承すること

継承を意図するクラスを設計することはかなりの制限を課す。

インタフェースの骨格実装(項目18)のように継承のためのクラスが正しい場合もあるが、不変クラス(項目15)のように継承することが正しくない場合がある。

この問題に対しては、サブクラス化されるために設計・文書化されていないクラスでのサブクラス化を禁止することが解決策になる。 二つの方法があり、一つ目の方法はクラスを final にする。 もう一つはコンストラクタを private かパッケージプライベートにする。

他にもクラスから自己利用を取り除く方法として、個々のオーバーライド可能なメソッドの本体を private ヘルパーメソッドに移動させる方法がある。

感想

相変わらずの継承 dis だなぁ。(dis は言い過ぎ)

でも、それだけ継承は危険ということだろう。 考えてみれば確かに継承はカプセル化を破壊してるなー。 ということを分かったので、勉強している甲斐があったというものだ。

(c) The King's Museum