Java 表示式之謎:為什麼 index 增加了兩次?

專注的阿熊發表於2019-11-07

Code Golf中的一位挑戰者在比賽中寫了下面這段程式碼:(譯註:Code Golf是一個程式設計挑戰比賽,提交的程式碼越短越好)


import java.util.*;

public class Main {
  public static void main (String[] args) {
    int size = 3;
   String[] array = new String[size];
   Arrays.fill( array, "");
    for( int i = 0; i <= 100; ) {
      array[i++%size] += i + " ";
   }
    for(String element: array) {
     System.out.println(element);
   }
 }
}

在Java 8中執行程式碼,得到結果如下:


1 
4 
7 
10 
13 
16 
19 
22 
25 
28 
31 
34 
37 
40 
43 
46 
49 
52 
55 
58 
61 
64 
67 
70 
73 
76 
79 
82 
85 
88 
91 
94 
97 
100 

2 5 8 11 14 17 20 23 26 29 32 35 38 41 44 47 50 53 56 59 62 65 68 71 74 77 80 83 86 89 92 95 98 101
3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99

在Java 10中執行程式碼,得到結果如下:


2 
4 
6 
8 
10 
12 
14 
16 
18 
20 
22 
24 
26 
28 
30 
32 
34 
36 
38 
40 
42 
44 
46 
48 
50 
52 
54 
56 
58 
60 
62 
64 
66 
68 
70 
72 
74 
76 
78 
80 
82 
84 
86 
88 
90 
92 
94 
96 
98 

2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 102
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100

在Java 10中編號似乎完全失效了。這中間發生了什麼?這是Java 10的bug嗎?

來自評論區的討論:

用Java 9或更高版本編譯會出現問題(我們在Java 10中找到了問題)。在Java 8上編譯這段程式碼,然後在Java 9或更高版本(包括Java 11 EA)中執行,可以得到預期結果。

雖然這種程式碼不標準,但符合Java規範。Kevin Cruijssen在一個Code Golf挑戰中發現了這個問題,看起來結果很奇怪。

Didier L發現可以用更短、更容易理解的程式碼重現該問題:


class Main {

  public static void main (String[] args) {
   String[] array = { "" };
    array[test()] += "a";
 }
  static int test () {
   System.out.println( "evaluated");
    return 0;
 }
}

用Java 8編譯,執行結果:

evaluated

用Java 9和10編譯,執行結果

evaluated

evaluated

問題似乎與字串連線操作和賦值運算子( += )有關,當作為左運算子時會出現副作用,例如 array[test()]+="a" array[ix++]+="a" test()[index]+="a" test().field+="a" 。字串連線要求至少有一邊的物件型別為String。其他型別或結構無法復現該錯誤。

答案

這是JDK 9開始引入的一個javac bug(疑似在字串拼接過程中進行了修改),已由javac團隊確認,bug id  JDK-8204322。檢視該行對應的位元組碼:

array[i++%size] += i + " ";

位元組碼:


  21: aload_2

  22: iload_3
  23: iinc           3, 1
  26: iload_1
  27: irem
  28: aload_2
  29: iload_3
  30: iinc           3, 1
  33: iload_1
  34: irem
  35: aaload
  36: iload_3
  37: invokedynamic # 5,   0 // makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
  42: aastore

最後的 aaload 從陣列中實際載入資料。但是,下面這段


  21

: aload_2             
// load 陣列引用



 
22 : iload_3             // load 'i' function(){   //外匯跟單   23 : iinc           3 , 1   // 'i' 加1  (不影響已載入的陣列值)
 
26 : iload_1             // load 'size'
 
27 : irem                 // 計算餘數

基本上能與 array[i++%size] 表示式對應(去掉實際的load和store),問題是這裡出現了兩次。 按照jls-15.26.2規範中的描述,這是不正確的:

複合表示式 E1 op= E2 E1 = (T) ((E1) op (E2)) 等價,其中T的型別是E1, 除了E1應該只執行一次。

因此,表示式 array[i++%size] += i + " "; array[i++%size] 應該只計算一次。 但是這裡會計算兩次(load一次,store一次)。

可以確認,這是一個bug。

更新:

該bug已在JDK 11中修復,並且對應更新到JDK 10(但JDK 9不會修復,因為它不再進行public updates)。

Aleksey ShipilevJBS 頁面上提到(@DidierL在此進行了評論):

解決方法 :使用 -XDstringConcat=inline 編譯。

這樣會使用 StringBuilder 進行字串連線,不會出現該bug。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2663017/,如需轉載,請註明出處,否則將追究法律責任。

相關文章