代码之家  ›  专栏  ›  技术社区  ›  Olivier Grégoire

为什么数组[IDx++] +=“a”在Java 8中增加了一次IDX,但在Java 9和10中增加了两次?

  •  721
  • Olivier Grégoire  · 技术社区  · 7 年前

    为了挑战, a fellow code golfer wrote the following code :

    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);
        }
      }
    }
    

    When running this code in Java 8, we get the following result:

    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 
    

    When running this code in Java 10, we get the following result:

    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的早期访问,给出预期的结果。
    • 这种代码是非标准的,但根据规范是有效的。它是由 Kevin Cruijssen 在一个 discussion in a golfing challenge ,因此遇到了奇怪的用例。
    • Didier L 发现这个问题可以用更小更容易理解的代码重现:

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

      Result when compiled in Java 8:

      evaluated
      

      Result when compiled in Java 9 and 10:

      evaluated
      evaluated
      
    • 这个问题似乎仅限于字符串连接和赋值运算符。( += )使用副作用为左操作数的表达式,如 array[test()]+="a" , array[ix++]+="a" , test()[index]+="a" test().field+="a" . 若要启用字符串连接,至少有一个边必须具有类型 String . 尝试在其他类型或构造上复制此内容失败。

    1 回复  |  直到 7 年前
        1
  •  611
  •   Juan Moreno    7 年前

    这是个窃听器 javac 从jdk 9(它对字符串连接做了一些更改,我怀疑这是问题的一部分)开始, as confirmed by the javac team under the 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 the array reference
      22: iload_3             // load 'i'
      23: iinc          3, 1  // increment 'i' (doesn't affect the loaded value)
      26: iload_1             // load 'size'
      27: irem                // compute the remainder
    

    大致对应于 array[i++%size] (减去实际的负载和存储),是在那里两次。这是不正确的,正如规范中所说 jls-15.26.2 :

    形式的复合赋值表达式 E1 op= E2 相当于 E1 = (T) ((E1) op (E2)) 在哪里 T E1 , 除了那个 E1 只评估一次。

    所以,为了表达 array[i++%size] += i + " "; 部分 数组[i++%大小] 只应评估一次。但它会被评估两次(一次用于加载,一次用于存储)。

    所以是的,这是一只虫子。


    一些更新:

    这个bug在jdk 11中被修复了,将会有一个 back-port to JDK 10 (但不是JDK 9,因为 it no longer receives public updates )

    Aleksey Shipilev提到 JBS page (以及此处评论中的@didierr):

    解决方法:编译 -XDstringConcat=inline

    将恢复为使用 StringBuilder 做连接,没有错误。

    推荐文章