Java における定数展開

C++ の方は、以前バカが征くでもやってましたね。まぁ、それはよくて。

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

インスタンスフィールドの初期化が特別扱いされているようなことはないと思う。コードの上では別でもコンパイルされると、各コンストラクタに織り込まれるんでは。

ちなみに,インスタンスフィールドにfinalをつけておくと,挙動が変化する。例えばC2クラスを,
(snip)
というように変更してあげれば,「new C2();」の実行結果は,
(snip)
というように,foo()メソッドの呼び出しの前に評価が済んでいる状態となる。ただし,これはインスタンスフィールドの型がプリミティブな場合のみ。オブジェクト型の場合は,finalをつけたとしても,相変わらず初期化が遅延される。

これも違うと思う。final つけたときだけ初期化の順序が異なるなんて話は聞いたことがない。Java で public static final な定数はコンパイル時にインライン展開されるというのは有名な話で*1、たとえば

public class Foo {
    public static final int A = 0;
}
public class Bar {
    public static void main(String[] args) {
        System.out.println(Foo.A);
    }
}

とあったときに、Foo.A の値が変更されたときは Bar.java もリコンパイルが必要だったりするんだけども*2、これと同じことでは。
と、推測だけ言ってもアレなので、確認してみる。

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

    abstract void foo();
}

class C2 extends C1 {
    private int value = 1;

    void foo() {
        System.out.println("1: " + value);
    }
}

class C3 extends C1 {
    private final int value = 1;

    void foo() {
        System.out.println("1: " + value);
    }
}

class C4 extends C1 {
    private final String value = "1";

    void foo() {
        System.out.println("1: " + value);
    }
}

class C5 extends C1 {
    private final String value = new String("1");

    void foo() {
        System.out.println("1: " + value);
    }
}

public class test {
    public static void main(String[] args) {
        new C2();
        new C3();
        new C4();
        new C5();
    }
}
% javac test.java
% java test
1: 0
1: 1
1: 1
1: null

と、まぁこういう結果なんだけど、逆アセンブルして、バイトコードを見てみる。

% javap -c C2 C3 C4 C5

Compiled from "test.java"
class C2 extends C1{
C2();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method C1."<init>":()V
   4:   aload_0
   5:   iconst_1
   6:   putfield        #2; //Field value:I
   9:   return

void foo();
  Code:
   0:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   new     #4; //class java/lang/StringBuilder
   6:   dup
   7:   invokespecial   #5; //Method java/lang/StringBuilder."<init>":()V
   10:  ldc     #6; //String 1:
   12:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  aload_0
   16:  getfield        #2; //Field value:I
   19:  invokevirtual   #8; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   22:  invokevirtual   #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   25:  invokevirtual   #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   28:  return

}

Compiled from "test.java"
class C3 extends C1{
C3();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method C1."<init>":()V
   4:   aload_0
   5:   iconst_1
   6:   putfield        #2; //Field value:I
   9:   return

void foo();
  Code:
   0:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #4; //String 1: 1
   5:   invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return

}

Compiled from "test.java"
class C4 extends C1{
C4();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method C1."<init>":()V
   4:   aload_0
   5:   ldc     #2; //String 1
   7:   putfield        #3; //Field value:Ljava/lang/String;
   10:  return

void foo();
  Code:
   0:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #5; //String 1: 1
   5:   invokevirtual   #6; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return

}

Compiled from "test.java"
class C5 extends C1{
C5();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method C1."<init>":()V
   4:   aload_0
   5:   new     #2; //class java/lang/String
   8:   dup
   9:   ldc     #3; //String 1
   11:  invokespecial   #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
   14:  putfield        #5; //Field value:Ljava/lang/String;
   17:  return

void foo();
  Code:
   0:   getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   new     #7; //class java/lang/StringBuilder
   6:   dup
   7:   invokespecial   #8; //Method java/lang/StringBuilder."<init>":()V
   10:  ldc     #9; //String 1:
   12:  invokevirtual   #10; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  aload_0
   16:  getfield        #5; //Field value:Ljava/lang/String;
   19:  invokevirtual   #10; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   22:  invokevirtual   #11; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   25:  invokevirtual   #12; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   28:  return

}

うん、やっぱり定数展開されているだけだ。初期化の順序の問題ではない。
インスタンスフィールドの初期化もコンストラクタの中で且つスーパークラスのコンストラクタ呼び出し後ですね。final、非 final 関係なく。

*1:最近ではそうでもないかも知れないが

*2:Ant 任せなんかだと案外はまる