1. ホーム
  2. java

[解決済み] コンストラクタでオーバーライド可能なメソッド呼び出しの何が問題なのでしょうか?

2022-03-26 04:42:35

質問

抽象的なメソッドの結果に応じてページタイトルを設定するWicketページクラスがあります。

public abstract class BasicPage extends WebPage {

    public BasicPage() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();

}

NetBeansは、"Overridable method call in constructor"というメッセージで警告してきますが、何が問題なのでしょうか?私が想像できる唯一の代替案は、サブクラスでスーパーコンストラクタに他の抽象的なメソッドの結果を渡すことです。しかし、これではパラメータが多くて読みにくいかもしれません。

どうすればいい?

コンストラクタからオーバーライド可能なメソッドを呼び出す場合

簡単に言うと、これは間違っています。 MANY バグが発生します。このような場合 @Override を呼び出すと、オブジェクトの状態に矛盾が生じたり、不完全な状態になることがあります。

引用元 Effective Java 2nd Edition, Item 17: Design and document for inheritance, or else prohibit it. :

継承を可能にするために、クラスが従わなければならない制限がいくつかあります。 コンストラクタはオーバーライド可能なメソッドを呼び出してはならない。 直接、間接を問わず。このルールに違反した場合、プログラムは失敗します。スーパークラスのコンストラクタはサブクラスのコンストラクタの前に実行されるため、サブクラスのオーバーライド メソッドはサブクラスのコンストラクタが実行される前に呼び出されます。オーバーライドするメソッドがサブクラスのコンストラクタによって実行される初期化に依存する場合、そのメソッドは期待どおりに動作しません。

以下に例を挙げて説明します。

public class ConstructorCallsOverride {
    public static void main(String[] args) {

        abstract class Base {
            Base() {
                overrideMe();
            }
            abstract void overrideMe(); 
        }

        class Child extends Base {

            final int x;

            Child(int x) {
                this.x = x;
            }

            @Override
            void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}

ここで Base コンストラクタは overrideMe , Child は初期化を終えていません。 final int x で、このメソッドは間違った値を取得します。これは、ほぼ間違いなくバグやエラーの原因となります。

関連する質問

こちらもご覧ください


多くのパラメータを持つオブジェクトの構築について

多くのパラメータを持つコンストラクタは可読性を低下させる可能性があり、より良い代替案が存在します。

以下、引用します。 Effective Java 第2版、項目2:コンストラクタのパラメータが多い場合はビルダパターンを考慮する :

従来、プログラマは 伸縮自在のコンストラクタ このパターンでは、あるコンストラクタには必須のパラメータだけを与え、別のコンストラクタにはオプションのパラメータをひとつ与え、3番目のコンストラクタにはオプションのパラメータをふたつ与え......といった具合に指定します。

telescopingコンストラクタのパターンは、基本的にこのようなものです。

public class Telescope {
    final String name;
    final int levels;
    final boolean isAdjustable;

    public Telescope(String name) {
        this(name, 5);
    }
    public Telescope(String name, int levels) {
        this(name, levels, false);
    }
    public Telescope(String name, int levels, boolean isAdjustable) {       
        this.name = name;
        this.levels = levels;
        this.isAdjustable = isAdjustable;
    }
}

そして、次のいずれかを行うことができるようになりました。

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);

ただし、現在は nameisAdjustable を残して levels をデフォルトで使用します。コンストラクタのオーバーロードを増やすことはできますが、パラメータの数が増えれば当然その数は爆発的に増えますし、さらに複数の booleanint という引数があると、本当に物事がめちゃくちゃになります。

見ての通り、これは書いていて気持ちの良いパターンではありませんし、使っていても気持ちの良いものではありません(ここで "true" はどういう意味? 13は何?)。

Blochはbuilderパターンを使うことを推奨しており、そうすれば代わりにこのようなことが書ける。

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();

パラメータには名前がついていて、好きな順番で設定でき、デフォルト値のままにしておきたいパラメータはスキップできることに注意してください。特に、同じ型に属するパラメータが大量にあるような場合、コンストラクタを拡張するよりもずっと良い方法です。

こちらもご覧ください

関連する質問