あまり知られていないと思われる Java のインスタンスイニシャライザの紹介

スタティックイニシャライザというのは Java をやってる人なら何度も使ったことがあると思う。

Java を 10 年以上やっていて「インスタンスイニシャライザ」というものがあることを先日知った。

public class Test {
  int i;

  {
    // このブロックがインスタンスイニシャライザ
    System.out.println("1:" + i);
    i = Math.max(1, 2);
    System.out.println("2:" + i);
  }

  public Test() {
  }

  public Test(int i) {
    this.i = i;
  }
}

このコードをコンパイルして逆コンパイルすると、以下のようなコードになる。

public class Test
{
  int i;

  public Test()
  {
    System.out.println("1:" + this.i);
    this.i = Math.max(1, 2);
    System.out.println("2:" + this.i);
  }

  public Test(int paramInt)
  {
    System.out.println("1:" + this.i);
    this.i = Math.max(1, 2);
    System.out.println("2:" + this.i);

    this.i = paramInt;
  }
}

インスタンスイニシャライザに記載した処理がすべてのコンストラクタにコピーされる。

「えっ、初期化処理をメソッド化して、コンストラクタから呼び出せばいいじゃん」と思うかもしれない。多くの場合は正しい考え方だけど、インスタンスイニシャライザが有効に機能するケースも存在する。

無名クラスで初期化処理を行いたい場合

以下のケースを見てみよう。

public class Test2 {
  Runnable runnable = new Runnable() {
    int i;
    {
      System.out.println(i);
      i = Math.max(1, 2);
      System.out.println(i);
    }

    public void run() {
    }
  };
}

このコードから生成されたクラスファイルは、手持ちの Java Decompiler では逆コンパイルできないので、javap を使用する。生成された無名クラスを javap で見てみると、まんまと無名クラスのコンストラクタに処理が記述されていた。


インスタンスイニシャライザはおそらくこのためにあるんだと思うんだけど、もう少し追求してみた。


継承関係にあるクラスのサブクラスでインスタンスイニシャライザを使用して、スーパークラスのコンストラクタを呼び出す前にインスタンスイニシャライザで処理ができないかと思って試してみたが、インスタンスイニシャライザは、スーパークラスのコンストラクタ呼び出しの後かつコンストラクタコードの前に実行されることがわかった。以下が許されないのと一緒で、スーパーコンストラクタの呼び出しは絶対に一番最初でなければならない。

public class Test3 extends Parent {
  String s = "hoge";
  Test3() {
    super(s); // コンパイルエラー。s はまだ初期化されていない
  }
}
インスタンスイニシャライザまとめ
  • フィールドの位置にブロックで初期化処理が書ける
  • 処理はすべてのコンストラクタにコピーされる
    • 位置はスーパーコンストラクタ呼び出しの直後
  • インスタンスイニシャライザは複数書ける
    • 上から順に処理される
  • コンストラクタが定義できない無名クラスと相性が良い
  • インスタンスイニシャライザの仕様は Java Language Specification §8.6 に記載されている