Androidの某ライブラリで普通に使われてたので。
問題
突然ですが、皆さんは以下の実装の出力結果がわかりますか?
public class Main {
static class Foo {
public Foo() {
init();
}
protected void init() {
}
}
static class Bar extends Foo {
private int val1;
private int val2 = 0;
@Override
protected void init() {
val1 = 1;
val2 = 2;
}
}
public static void main(String... args) {
Bar bar = new Bar();
System.out.println(bar.val1);
System.out.println(bar.val2);
}
}
この出力結果は以下になります。
1
0
事象
Foo
クラスはコンストラクタで、init()
メソッドを呼び出しています。
Bar
クラスはFoo
のサブクラスで、Foo
の作ったinit()
メソッドをオーバーライドして、メンバ変数を初期化しています。
Bar.init()
でメンバ変数val1
, val2
にはそれぞれ、1
、2
が代入されています。
このBar
クラスをインスタンス化して、それぞれの値を出力させると、val1
は見たとおり1
が入っていますが、2
を代入したはずのval2
の値は0
となっています。
メンバ変数の初期化に潜む罠
この現象を紐解く鍵は2つあります。
1つはメンバ変数の定義、もうひとつはJavaにおける変数の初期化タイミングです。
メンバ変数の定義
問題のコードでは、Bar
クラスのもつメンバ変数は以下のように定義されています。
private int val1;
private int val2 = 0;
メンバ変数の定義について、明示的な初期化を行わない場合プリミティブであれば0
、クラスであればnull
が代入されます。
しかし、0
で初期化されると言っても、明示的に初期化をするかしないかでその振る舞いは変化します。
Javaのメンバ変数の初期化タイミング
Javaにおいてメンバ変数の初期化はコンストラクタで行われます。問題はコンストラクタ内のいつ行われているのかです。
普段なにげなく書いていると思いますが、じつはメンバ変数の初期化は親クラスのコンストラクタ呼び出し直後に行われます。
つまり
static class Foo {
public Foo() {
← ココ
init();
}
}
です。
何も見えないと思いますが、Javaのコンストラクタではコンストラクタの先頭で親クラスのコンストラクタの呼び出しが必須となっています。
ただし、これは省略できる条件があり、その条件が親クラスがデフォルトコンストラクタを定義していることです。
デフォルトコンストラクタとは、引数を取らないコンストラクタです。
(厳密には親のコンストラクタでなくてもいい場合もあるのですが、割愛します)
そのため、上のコードの場合、矢印の位置にはsuper()
呼び出しが存在しているのと同じことになります。
問題の記述
以上を踏まえて問題のコードで踏んだ罠について解説します。
罠を踏んだのはBar
クラスです。
static class Bar extends Foo {
private int val1;
private int val2 = 0;
@Override
protected void init() {
val1 = 1;
val2 = 2;
}
}
このクラスを省略せずに書くと以下のようになります。
static class Bar extends Foo {
private int val1;
private int val2 = 0;
public Bar() {
super();
}
@Override
protected void init() {
val1 = 1;
val2 = 2;
}
}
Bar
のコンストラクタではsuper()
、つまりFoo
のコンストラクタが呼び出されます。
Foo
のコンストラクタでは、init()
を呼び出していますが、これはBar
クラスがオーバーライドしているため、Bar.init()
の呼び出しになります。
このフローを雑に展開すると
Bar() {
super() {
Bar.init() {
Bar.val1 = 1;
Bar.val2 = 2;
}
}
Barのメンバ変数の初期化() {
Bar.val2 = 0;
}
}
という流れになります。
val1
はsuper()
後に0
で初期化されないのか?と思うかもしれませんが、定義時に初期化していないものは、super()
後の初期化処理自体が行われません。
補足
ちなみに、Bar.init()
のval2
を値代入前に参照すると0
が返ります。
メンバ定義の初期化に関係なくすべてのメンバ変数は0
またはnull
が代入されていて、super()
後に明示した値が代入されると思ってください。
そもそも、最初に貼った実装は糞コードなので、継承を理解してからオーバーライド使おうな?
参考
(公式に書いてあったはずだけど、リンクを失念しました。見つけたら貼ります…)