昨日のJJUG ナイトセミナーで出題されて正解者が少なかったAnniversary.javaの問題をjavapして、どのようにコードが実行されているか見てみようというだけのエントリーです。
なお、
さくらばさん「実はこれてらだよしおも間違えました」
#jjug #てらだよしおがんばれ
— 持田真哉 (@mike_neck) April 24, 2015
だそうです。
問題文
以下の問題を実行した時に標準出力に出力されるのはどれ?
import java.time.*; public class Anniversary { private static final Anniversary ANN = new Anniversary(); private static final int BIRTH = Year.of(1995).getValue(); private static final int NOW = Year.now().getValue(); private final int age; public int getAge() { return age; } public Anniversary() { this.age = NOW - BIRTH; } public static void main(String... args) { System.out.println("the age is " + ANN.getAge() + "."); } }
選択肢
- the age is 0.
- the age is 20.
- the age is 1995.
- 例外が発生
javapしてみる
上記のクイズの正解は1.です。
では、バイトコードがどのように実行されていくか、一つずつ見て行きたいと思います。
上記のコードをコンパイルしてjavap -c -vした結果は次のようになります。
Classfile /path/to/Anniversary.class
Last modified 2015/04/25; size 1061 bytes
MD5 checksum 11bad503406cc2ec015ddc95d15a520c
Compiled from "Anniversary.java"
public class Anniversary
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Fieldref #16.#39 // Anniversary.age:I
#2 = Methodref #21.#40 // java/lang/Object."<init>":()V
#3 = Fieldref #16.#41 // Anniversary.NOW:I
#4 = Fieldref #16.#42 // Anniversary.BIRTH:I
#5 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Class #45 // java/lang/StringBuilder
#7 = Methodref #6.#40 // java/lang/StringBuilder."<init>":()V
#8 = String #46 // the age is
#9 = Methodref #6.#47 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Fieldref #16.#48 // Anniversary.ANN:LAnniversary;
#11 = Methodref #16.#49 // Anniversary.getAge:()I
#12 = Methodref #6.#50 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#13 = String #51 // .
#14 = Methodref #6.#52 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#15 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #55 // Anniversary
#17 = Methodref #16.#40 // Anniversary."<init>":()V
#18 = Methodref #56.#57 // java/time/Year.of:(I)Ljava/time/Year;
#19 = Methodref #56.#58 // java/time/Year.getValue:()I
#20 = Methodref #56.#59 // java/time/Year.now:()Ljava/time/Year;
#21 = Class #60 // java/lang/Object
#22 = Utf8 ANN
#23 = Utf8 LAnniversary;
#24 = Utf8 BIRTH
#25 = Utf8 I
#26 = Utf8 NOW
#27 = Utf8 age
#28 = Utf8 getAge
#29 = Utf8 ()I
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 <init>
#33 = Utf8 ()V
#34 = Utf8 main
#35 = Utf8 ([Ljava/lang/String;)V
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 Anniversary.java
#39 = NameAndType #27:#25 // age:I
#40 = NameAndType #32:#33 // "<init>":()V
#41 = NameAndType #26:#25 // NOW:I
#42 = NameAndType #24:#25 // BIRTH:I
#43 = Class #61 // java/lang/System
#44 = NameAndType #62:#63 // out:Ljava/io/PrintStream;
#45 = Utf8 java/lang/StringBuilder
#46 = Utf8 the age is
#47 = NameAndType #64:#65 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = NameAndType #22:#23 // ANN:LAnniversary;
#49 = NameAndType #28:#29 // getAge:()I
#50 = NameAndType #64:#66 // append:(I)Ljava/lang/StringBuilder;
#51 = Utf8 .
#52 = NameAndType #67:#68 // toString:()Ljava/lang/String;
#53 = Class #69 // java/io/PrintStream
#54 = NameAndType #70:#71 // println:(Ljava/lang/String;)V
#55 = Utf8 Anniversary
#56 = Class #72 // java/time/Year
#57 = NameAndType #73:#74 // of:(I)Ljava/time/Year;
#58 = NameAndType #75:#29 // getValue:()I
#59 = NameAndType #76:#77 // now:()Ljava/time/Year;
#60 = Utf8 java/lang/Object
#61 = Utf8 java/lang/System
#62 = Utf8 out
#63 = Utf8 Ljava/io/PrintStream;
#64 = Utf8 append
#65 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#66 = Utf8 (I)Ljava/lang/StringBuilder;
#67 = Utf8 toString
#68 = Utf8 ()Ljava/lang/String;
#69 = Utf8 java/io/PrintStream
#70 = Utf8 println
#71 = Utf8 (Ljava/lang/String;)V
#72 = Utf8 java/time/Year
#73 = Utf8 of
#74 = Utf8 (I)Ljava/time/Year;
#75 = Utf8 getValue
#76 = Utf8 now
#77 = Utf8 ()Ljava/time/Year;
{
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #1 // Field age:I
4: ireturn
LineNumberTable:
line 11: 0
public Anniversary();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #3 // Field NOW:I
8: getstatic #4 // Field BIRTH:I
11: isub
12: putfield #1 // Field age:I
15: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 15
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #6 // class java/lang/StringBuilder
6: dup
7: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
10: ldc #8 // String the age is
12: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: getstatic #10 // Field ANN:LAnniversary;
18: invokevirtual #11 // Method getAge:()I
21: invokevirtual #12 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
24: ldc #13 // String .
26: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: return
LineNumberTable:
line 19: 0
line 20: 35
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #16 // class Anniversary
3: dup
4: invokespecial #17 // Method "<init>":()V
7: putstatic #10 // Field ANN:LAnniversary;
10: sipush 1995
13: invokestatic #18 // Method java/time/Year.of:(I)Ljava/time/Year;
16: invokevirtual #19 // Method java/time/Year.getValue:()I
19: putstatic #4 // Field BIRTH:I
22: invokestatic #20 // Method java/time/Year.now:()Ljava/time/Year;
25: invokevirtual #19 // Method java/time/Year.getValue:()I
28: putstatic #3 // Field NOW:I
31: return
LineNumberTable:
line 4: 0
line 5: 10
line 6: 22
}
SourceFile: "Anniversary.java"
staticイニシャライザー
まずはstaticイニシャライザーの部分から。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #16 // class Anniversary
3: dup
4: invokespecial #17 // Method "<init>":()V
7: putstatic #10 // Field ANN:LAnniversary;
10: sipush 1995
13: invokestatic #18 // Method java/time/Year.of:(I)Ljava/time/Year;
16: invokevirtual #19 // Method java/time/Year.getValue:()I
19: putstatic #4 // Field BIRTH:I
22: invokestatic #20 // Method java/time/Year.now:()Ljava/time/Year;
25: invokevirtual #19 // Method java/time/Year.getValue:()I
28: putstatic #3 // Field NOW:I
31: return
LineNumberTable:
line 4: 0
line 5: 10
line 6: 22
(1) 0: new #16
- テーブルの
#16(class Anniversary)に対して、new命令をします。- 操作内容 - 新たなオブジェクトを生成する
- スタックの状態
[]=>[objectref(Anniversary)]
(2) 3: dup
dup命令を実行します。- 操作内容 - オペランドスタックの先頭にある値を複製します。
- スタックの状態
[objectref(Anniversary)]=>[objectref(Anniversary), objectref(Anniversary)]
(3) 4: invokespecial #17
invokespecial命令を実行します。
ここで、処理はコンストラクターに移動します。
public Anniversary();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #3 // Field NOW:I
8: getstatic #4 // Field BIRTH:I
11: isub
12: putfield #1 // Field age:I
15: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 15
(4) 0: aload_0
aload_0命令を実行します。- 操作内容 - ローカル変数からreferenceをロードします。
- スタックの状態
[]=>[objectref(this)]
(5) 1: invokespecial #2
(6) 4: aload_0
aload_0命令(既出)を実行します。- 操作内容 - ローカル変数からreferenceをロードします。
- スタックの状態
[]=>[objectref(this)]
(7) 5: getstatic #3
getstatic命令を実行します。- 操作内容 - クラスのstaticフィールド(
NOW)を取得します。 - スタックの状態
[objectref(this)]=>[objectref(this), value(Field NOW:Int)]
- 操作内容 - クラスのstaticフィールド(
(8) 8: getstatic #4
getstatic命令(既出)を実行します- 操作内容 - クラスのstaticフィールド(
BIRTH)を取得します。 - スタックの状態
[objectref(this), value(Field NOW:Int)]=>[objectref(this), value(Field NOW:Int), value(Field BIRTH:Int)]
- 操作内容 - クラスのstaticフィールド(
(9) 11: isub
isub命令を実行します。- 操作内容 - オペランドスタックから二つ値をポップして、先頭のものから後ろのものを減算する
- スタックの状態
[objectref(this), value(Field NOW:Int), value(Field BIRTH:Int)]=>[objectref(this), result(Int)]
(10) 12: putfield #1
putfield命令を実行します- 操作内容 - オペランドスタックから二つ値をポップして、先頭の参照のフィールド
#1( = Anniversary.age)に値を格納します - スタックの状態
[objectref(this), result(Int)]=>[]
- 操作内容 - オペランドスタックから二つ値をポップして、先頭の参照のフィールド
(11) 15: return
これによって、staticイニシャライザーに戻ります。
(12) 7: putstatic #10
putstatic命令を実行します- 操作内容 - オペランドスタックから値を取り出して、クラスのstaticフィールド
#10 ( = Anniversary.ANN)に値を設定します - スタックの状態
[objectref(Anniversary)]=>[]
- 操作内容 - オペランドスタックから値を取り出して、クラスのstaticフィールド
(13) 10: sipush 1995
(14) 13: invokestatic #18
invokestatic命令を実行します- 操作内容 - テーブル
#18( = java/time/Year.of:(I))のstaticメソッドを起動します - スタックの状態
[1995]=>[objectref(java/time/Year)]
- 操作内容 - テーブル
【追記 2015/04/26 17:08】
呼び出された方のjava/time/Year.of:(I)では最後にareturn命令が実行されるため、起動側スタックにobjectrefがプッシュされる。
aretrun- 操作内容 - メソッドからreferenceをリターンする
(15) 16: invokevirtual #19
invokevirtual命令を実行します- 操作内容 - テーブル
#19 ( = java/time/Year.getValue:()I)を起動します - スタックの状態
[objectref(java/time/Year)]=>[value(int)]
- 操作内容 - テーブル
僕の持っているJava仮想マシン仕様第二版だと、オペランドスタックからobjectrefを取ってきて実行するように書かれているけど、今のスタックの状態だとオペランドスタック空っぽですよね…直前の結果を持っているスタック以外の1個だけ保持できる記憶領域あるのかしら、それとも(14)のinvokestaticでオペランドスタックに一度積んでいるのかしら…(´・ω・`)<(仮想マシンまだよくわかってないマンです)
【追記 2015/04/26 17:08】
呼び出されたjava/time/Year.getValue:()Iで最後にireturn命令が実行されて、起動側スタックにintの値がプッシュされる。
(16) 19: putstatic #4
putstatic命令(既出)を実行します- 操作内容 - オペランドスタック
(空だよね…)から値を取り出し、テーブル#4( = Anniversary.BIRTH:I)に値を格納します - スタックの状態
[value(int)]=>[]
- 操作内容 - オペランドスタック
(17) 22: invokestatic #20
invokestatic命令(既出)を実行します- 操作内容 - テーブル
#20 ( = java/time/Year.now:())を起動します。 - スタックの状態
[]=>[objectref(java/time/Year)]
- 操作内容 - テーブル
【追記 2015/04/26 17:08】
呼び出されたjava/time/Year.now:()が最後にareturnを実行して、起動側スタックにobjectrefがプッシュされる。
(18) 25: invokevirtual #19
invokevirtual命令(既出)を実行します- 操作内容 - テーブル
#19 ( = java/time/Year.getValue:()I)を起動します(既出) - スタックの状態
[objectref(java/time/Year)]=>[value(int)]
- 操作内容 - テーブル
【追記 2015/04/26 17:08】
呼び出されたjava.time/Year.getValue:()Iが最後にireturnを実行して、起動側スタックにintの値がプッシュされる。
(19) 28: putstatic #3
putstatic命令(既出)を実行します- 操作内容 - オペランドスタック(空だよね…)から値を取り出し、テーブル
#3( = Anniversary.NOW:I)に値を格納します - スタックの状態
[value(int)]=>[]
- 操作内容 - オペランドスタック(空だよね…)から値を取り出し、テーブル
(20) 31: return
return命令(既出)を実行します。- 操作内容 - メソッドからvoidをリターンします
- スタックの状態
[]=>[]
バイトコードを読んでみて
上記の操作(7)および(8)を見るとわかるように、staticフィールドの初期化で、とりあえずfinalなフィールドでも参照ならnull、プリミティブなら0で初期化されるという話と合わせて考えると、上記のクイズで答えが0になるのも納得できますね。
staticフィールドの初期化、finalであっても最初はnullが入る(オブジェクトの場合) #jjug
— けーえむ@氷見ブリ会場探してます (@kamekoopa) April 24, 2015
staticフィールドの初期化、ただしJavaコードの実行はしないので、static final List<String> list = new ArrayList<>()となっててもnullが入る #jjug
— 持田真哉 (@mike_neck) April 24, 2015
結論
バイトコード読むの面白いのでやってみるべき。
参考文献
【修正 2015/04/27 12:39】
命令へのリンクがType Checking Instructionsに向かっていたのを全面的にMachine Instruction Setsに向かうように修正した。