抽象メソッドの結果に応じてページタイトルを設定するWicketページクラスがあります。
public abstract class BasicPage extends WebPage {
public BasicPage() {
add(new Label("title", getTitle()));
}
protected abstract String getTitle();
}
NetBeansは「コンストラクタ内でオーバーライド可能なメソッド呼び出し」というメッセージを出して私に警告しますが、何が悪いのでしょうか?私が想像できる唯一の代替策は、それ以外の場合は抽象メソッドの結果をサブクラスのスーパーコンストラクターに渡すことです。しかし、それは多くのパラメータで読むのが難しいかもしれません。
端的に言って、これは不必要にMANYバグの可能性を開くので間違っています。 @Override
が呼び出されると、オブジェクトの状態は矛盾したり不完全になったりする可能性があります。
Effective Java 2nd Edition、Item 17からの引用:継承のための設計と文書化、さもなければそれを禁止する:
継承を許可するためにクラスが従わなければならないいくつかの制限があります。 コンストラクタは、オーバーライド可能なメソッドを直接的または間接的に呼び出してはいけません。この規則に違反すると、プログラムが失敗します。スーパークラスコンストラクターはサブクラスコンストラクターの前に実行されるため、サブクラスのオーバーライドメソッドはサブクラスコンストラクターが実行される前に呼び出されます。オーバーライドするメソッドがサブクラスコンストラクタによって実行される初期化に依存している場合、そのメソッドは期待どおりに動作しません。
これは説明のための例です。
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 2nd Edition、Item 2からの引用です:多くのコンストラクタパラメータに直面したときビルダーパターンを考えます:
伝統的に、プログラマーは入れ子式コンストラクターパターンを使用しました。これは、必須パラメーターのみを持つコンストラクター、単一のオプション・パラメーターを持つコンストラクター、2つのオプション・パラメーターを含む3番目のコンストラクターなどを提供します。に...
伸縮するコンストラクタパターンは、基本的にこのようなものです。
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);
ただし、現時点ではname
とisAdjustable
のみを設定し、デフォルトではlevels
のままにすることはできません。より多くのコンストラクタオーバーロードを提供することができますが、明らかにパラメータの数が増えるにつれてその数は爆発します、そしてあなたは複数のboolean
とint
引数さえ持つかもしれません。
お分かりのように、これは書くのに楽しいパターンではなく、使用するのがそれほど快適でもありません(「本当の」とはどういう意味ですか?13とは何ですか?)。
Blochはあなたが代わりにこのような何かを書くことを可能にするビルダーパターンを使うことを勧めます:
Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();
これで、パラメータに名前が付けられ、好きな順番で設定でき、デフォルト値のままにしておきたいものをスキップすることができます。これは、コンストラクタを入れ子にするよりも確かに優れています。特に、同じ型の多くに属する膨大な数のパラメータがある場合はそうです。
これを理解するのを助ける例はここにあります:
public class Main {
static abstract class A {
abstract void foo();
A() {
System.out.println("Constructing A");
foo();
}
}
static class C extends A {
C() {
System.out.println("Constructing C");
}
void foo() {
System.out.println("Using C");
}
}
public static void main(String[] args) {
C c = new C();
}
}
このコードを実行すると、次のような出力が得られます。
Constructing A
Using C
Constructing C
分かりますか? foo()
はCのコンストラクタが実行される前にCを利用します。 foo()
がCに定義された状態を持つことを要求するならば(すなわち、コンストラクタ終了した)、それはCで未定義の状態に遭遇し、事態は壊れるかもしれません。そしてあなたはAで上書きされたfoo()
が何を期待しているのかわからないので、あなたは警告を受ける。
コンストラクタでオーバーライド可能なメソッドを呼び出すと、サブクラスがコードを破壊することが可能になるため、それが機能することを保証することはできません。だから警告を受けるのです。
あなたの例では、サブクラスがgetTitle()
をオーバーライドしてnullを返すとどうなりますか?
これを「修正する」には、コンストラクタの代わりに ファクトリメソッド を使うことができます。これはオブジェクトのインスタンス化の一般的なパターンです。
サブクラスがオーバーライドするコンストラクター内のメソッドを呼び出す場合、初期化をコンストラクターとメソッドの間で論理的に分割すると、まだ存在しない変数を参照する可能性が低くなります。
このサンプルリンクをご覧ください http://www.javapractices.com/topic/TopicAction.do?Id=215
これは、スーパーコンストラクターでオーバーライド可能なメソッドを呼び出すときに発生する可能性のある論理問題を明らかにする例です。
class A {
protected int minWeeklySalary;
protected int maxWeeklySalary;
protected static final int MIN = 1000;
protected static final int MAX = 2000;
public A() {
setSalaryRange();
}
protected void setSalaryRange() {
throw new RuntimeException("not implemented");
}
public void pr() {
System.out.println("minWeeklySalary: " + minWeeklySalary);
System.out.println("maxWeeklySalary: " + maxWeeklySalary);
}
}
class B extends A {
private int factor = 1;
public B(int _factor) {
this.factor = _factor;
}
@Override
protected void setSalaryRange() {
this.minWeeklySalary = MIN * this.factor;
this.maxWeeklySalary = MAX * this.factor;
}
}
public static void main(String[] args) {
B b = new B(2);
b.pr();
}
結果は実際には次のようになります。
minWeeklySalary:0
maxWeeklySalary:0
これは、クラスBのコンストラクタが最初にクラスAのコンストラクタを呼び出すためです。ここでは、B内のオーバーライド可能メソッドが実行されます。しかしメソッド内では、(Aのコンストラクタがまだ終了していないため)インスタンス変数factorまだ初期化されていないを使用しているため、factorは0で1ではなくそして間違いなく2ではありません(プログラマーがそれがなるだろうと思うかもしれないこと)。計算ロジックが10倍もねじれている場合、エラーを追跡するのがどれほど難しいかを想像してください。
それが誰かに役立つことを願っています。
Wicketの特定の場合:これが、Wicket開発者に、コンポーネントを構築するフレームワークのライフサイクルにおける明示的な2フェーズコンポーネント初期化プロセスのサポートを追加するよう依頼したまさにその理由です。
このリンクが示すように http://Apache-wicket.1842946.n4.nabble.com/VOTE -WICKET-3218-Component-onInitialize-Pages-td3341090i20.htmlの破損 )
良いニュースは、Wicketの優秀な開発者たちが2段階の初期化を導入したことです(最も厄介なJava UIフレームワークをさらに素晴らしいものにするためです)。あなたがそれをオーバーライドするならフレームワークは自動的に - あなたのコンポーネントのライフサイクルのこの時点でそのコンストラクタはその仕事を完了したので仮想メソッドは期待通りに働く。
私はWicketのためにonInitialize()
の中でadd
メソッドを呼ぶほうがいいと思います( components lifecycle を参照):
public abstract class BasicPage extends WebPage {
public BasicPage() {
}
@Override
public void onInitialize() {
add(new Label("title", getTitle()));
}
protected abstract String getTitle();
}