昨日の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に向かうように修正した。