LoginSignup
13
14

More than 5 years have passed since last update.

Javaのコンストラクタで呼び出す変数初期化メソッドはオーバーライドしてはいけないという話

Posted at

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にはそれぞれ、12が代入されています。

このBarクラスをインスタンス化して、それぞれの値を出力させると、val1は見たとおり1が入っていますが、2を代入したはずのval2の値は0となっています。

メンバ変数の初期化に潜む罠

この現象を紐解く鍵は2つあります。

1つはメンバ変数の定義、もうひとつはJavaにおける変数の初期化タイミングです。

メンバ変数の定義

問題のコードでは、Barクラスのもつメンバ変数は以下のように定義されています。

Bar
private int val1;

private int val2 = 0;

メンバ変数の定義について、明示的な初期化を行わない場合プリミティブであれば0、クラスであればnullが代入されます。
しかし、0で初期化されると言っても、明示的に初期化をするかしないかでその振る舞いは変化します。

Javaのメンバ変数の初期化タイミング

Javaにおいてメンバ変数の初期化はコンストラクタで行われます。問題はコンストラクタ内のいつ行われているのかです。

普段なにげなく書いていると思いますが、じつはメンバ変数の初期化は親クラスのコンストラクタ呼び出し直後に行われます。

つまり

Foo
static class Foo {

    public Foo() {
                     ココ
        init();
    }
}

です。

何も見えないと思いますが、Javaのコンストラクタではコンストラクタの先頭で親クラスのコンストラクタの呼び出しが必須となっています。
ただし、これは省略できる条件があり、その条件が親クラスがデフォルトコンストラクタを定義していることです。
デフォルトコンストラクタとは、引数を取らないコンストラクタです。
(厳密には親のコンストラクタでなくてもいい場合もあるのですが、割愛します)

そのため、上のコードの場合、矢印の位置にはsuper()呼び出しが存在しているのと同じことになります。

問題の記述

以上を踏まえて問題のコードで踏んだ罠について解説します。

罠を踏んだのはBarクラスです。

Bar
static class Bar extends Foo {

    private int val1;

    private int val2 = 0;

    @Override
    protected void init() {
        val1 = 1;
        val2 = 2;
    }
}

このクラスを省略せずに書くと以下のようになります。

Bar
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;
  }
}

という流れになります。

val1super()後に0で初期化されないのか?と思うかもしれませんが、定義時に初期化していないものは、super()後の初期化処理自体が行われません。

補足

ちなみに、Bar.init()val2を値代入前に参照すると0が返ります。
メンバ定義の初期化に関係なくすべてのメンバ変数は0またはnullが代入されていて、super()後に明示した値が代入されると思ってください。

そもそも、最初に貼った実装は糞コードなので、継承を理解してからオーバーライド使おうな?

参考

(公式に書いてあったはずだけど、リンクを失念しました。見つけたら貼ります…)

13
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14