Posted at

【Java】後置インクリメントした変数を自己代入した場合の挙動

More than 1 year has passed since last update.


突然ですが問題です

以下のコードを実行した場合、出力される値はなんでしょうか?

package sandbox;

/**
* 後置インクリメントした変数を自己代入した場合の挙動確認
*
* @author seri
*/

public class SelfIncrement {

public static void main(String[] args) {
int foo = 10;
foo = foo++;
System.out.println(foo);
}

}

「こんなアホなコード書かないよ。。」

というツッコミはさておき、このコードの挙動、わかりますか?

演算子の優先度からいうと、"="の方が"++"より弱いので「foo = foo++;」は「foo++;」と同じ結果になりそうで、 実際質問されたときには最初「11になるんじゃないですか?」と軽く答えたのですが……

出力される結果は「10」になります。


解説

これはコードとにらめっこしていてもらちが明かないので、javap -cでアセンブラコードを見ながら追っていきます。

※といいつつ、以下はEclipseのクラスファイルエディタの結果です

// 次からコンパイル SelfIncrement.java (バージョン 1.7 : 51.0, スーパー・ビット)

public class sandbox.SelfIncrement {

// メソッド記述子 #6 ()V
// スタック: 1, ローカル: 1
public SelfIncrement();
0 aload_0 [this]
1 invokespecial java.lang.Object() [8]
4 return
行数:
[pc: 0, : 3]
ローカル変数テーブル:
[pc: 0, pc: 5] ローカル: this インデックス: 0 : sandbox.SelfIncrement

// メソッド記述子 #15 ([Ljava/lang/String;)V
// スタック: 2, ローカル: 2
public static void main(java.lang.String[] args);
0 bipush 10 // 1.オペランドスタックに10をプッシュ
2 istore_1 [foo] // 2.オペランドスタックの値を、
// "カレントフレームのローカル変数型配列のインデックス1"(以下、配列1)に設定
3 iload_1 [foo] // 3.配列1の値をオペランドスタックにプッシュ(このとき入れてるのは10)
4 iinc 1 1 [foo] // 4.配列1の値に1を加算(この段階で配列1の値は11)
7 istore_1 [foo] // 5.オペランドスタックの値(10)を配列1に設定(★ここで配列1の値が11から10に代わる)
8 getstatic java.lang.System.out : java.io.PrintStream [16]
11 iload_1 [foo] // 6.配列1の値をオペランドスタックにプッシュ
// (次に読みだされるスタック値が10になる⇒これが出力される)
12 invokevirtual java.io.PrintStream.println(int) : void [22]
15 return
行数:
[pc: 0, : 9]
[pc: 3, : 10]
[pc: 8, : 11]
[pc: 15, : 12]
ローカル変数テーブル:
[pc: 0, pc: 16] ローカル: args インデックス: 0 : java.lang.String[]
[pc: 3, pc: 16] ローカル: foo インデックス: 1 : int
}

アセンブラコード上に1~6の解説コメントを入れました。

参考)http://msugai.fc2web.com/java/perform/byte.html

まとめると、「出力元であるカレントスタックにインクリメント後の値がプッシュされていないため、11は出力されない」ということのようです。

ちなみに「foo++;」にした場合は、以下のように出力されます。

 0  bipush 10

2 istore_1 [foo]
3 iinc 1 1 [foo]
6 getstatic java.lang.System.out : java.io.PrintStream [16]
9 iload_1 [foo]
10 invokevirtual java.io.PrintStream.println(int) : void [22]
13 return

これだとインクリメント後の値がカレントスタックにプッシュされているので、11が出力されることになるようです。

アセンブラコードを見る前、実行結果から推測していたときは、インクリメント後のポインタ参照がおかしくなってるのかと思いましたが、この結果だとメモリリーク的なことにはならなそうですね。

……なんにせよ、相当微妙ですが。

明らかにバグ原因になりそうなこのコードに対して、Javaのコンパイラはエラーも警告も出してくれません。

ですがFindBugsではこのコードに対して警告を出してくれます。やはりどこかのタイミングでFindBugsは実行した方がいいですね。