天天看點

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

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編譯,運作結果:

用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。檢視該行對應的位元組碼:

位元組碼:

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'
  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。

原作者:ImportNew/唐尤華

原文連結: Java 表達式之謎:為什麼 index 增加了兩次?

原出處:公衆号

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