インスタンスフィールドが勝手に上書きされる!?

さて,以下の抽象クラスがあったとする。これは,コンストラクタの中からfoo()メソッドを呼び出すが,foo()メソッドはサブクラスで実装を提供することを意味している。

abstract class C1 { C1() { foo(); } abstract void foo(); }

そして,上記のクラスを継承した下記のクラスがあったとする。ここでは,インスタンスフィールドvalueを宣言し,宣言と同時に値1を代入するように記述している。コンストラクタ内でvalue値を表示し,foo()メソッドの実装では,まずvalue値を表示し,その後valueフィールドに値2を代入している。

class C2 extends C1 { private int value = 1; C2() { System.out.println(“1: “ + value); } void foo() { System.out.println(“2: “ + value); value = 2; } }

さて,「new C2();」と実行したとき,どのような処理結果になるか想像できるだろうか?答えは,これ。

2: 0 1: 1

つまり,

  • クラス継承の上位のデフォルトコンストラクタが呼び出される。この際,具象クラスのインスタンスフィールドは評価(実行)されない。

  • そして,その後に具象クラスのインスタンスフィールドの式が評価され,コンストラクタが実行される。

というシーケンスとなる(あくまでソースコード上の見かけの動きであり,厳密に言うと↑は嘘)。よって,親クラスのデフォルトコンストラクタから呼び出されたfoo()メソッド内でインスタンスフィールドに値をセットしたとしても,その後にインスタンスフィールドの右辺が評価されるために,上書きされてしまうのだ。

ま,こんな動作は,普通のプログラミングにおいて気にする必要はない。Wicketでは,Componentクラスのインスタンス生成のタイミングでリスナーを登録することが可能である。で,実際にはComponentクラスのデフォルトコンストラクタ内でリスナーの呼び出し処理が走るので,開発者が作成するWebPage,Panel,Formクラスのサブクラスに定義されたインスタンスフィールドは,リスナーの発火時点では初期化されてないから代入しても上書きされちゃうかも,ということになると気がついたという話である。

ちなみに,インスタンスフィールドにfinalをつけておくと,挙動が変化する。例えばC2クラスを,

class C2 extends C1 { private final int value = 1; C2() { System.out.println(“1: “ + value); } void foo() { System.out.println(“2: “ + value); // value = 2; } }

というように変更してあげれば,「new C2();」の実行結果は,

2: 1 1: 1

というように,foo()メソッドの呼び出しの前に 評価が済んでいる値を持った状態となる。 ただし,これはインスタンスフィールドの型がプリミティブな場合のみ。オブジェクト型の場合は,finalをつけたとしても,相変わらず初期化が遅延される。

なるほど,奥が深い。

追記(2007/3/28)

odzさんのご指摘通り,思いっきり嘘を書いてしまった。実際には,finalを付けたか付けないかは,初期化の順番が変わるのではなく,定数展開されるかどうかで実行結果が異なってくる,ということである。よく調べずに推測を書いてしまった点を反省。。。orz

このエントリーをはてなブックマークに追加

関連記事

2023年のRemap

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて