为什么数组[idx ++] + ="a"在Java 8中增加一次idx,在Java 9和10中增加两次?

Oli*_*ire 730 java javac java-8 java-9 java-10

对于挑战,一位代码高尔夫球手 编写了以下代码:

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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

在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 
Run Code Online (Sandbox Code Playgroud)

在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 
Run Code Online (Sandbox Code Playgroud)

编号完全取决于使用Java 10.那么这里发生了什么?这是Java 10中的错误吗?

从评论中跟进:

Jor*_*nee 616

这是javac从JDK 9开始的一个错误(它对字符串连接进行了一些更改,我怀疑这是问题的一部分),正如javac团队在错误ID JDK-8204322下所证实的那样.如果查看该行的相应字节码:

array[i++%size] += i + " ";
Run Code Online (Sandbox Code Playgroud)

它是:

  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
Run Code Online (Sandbox Code Playgroud)

最后一个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
Run Code Online (Sandbox Code Playgroud)

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

形式的化合物,赋值表达式E1 op= E2是等效于E1 = (T) ((E1) op (E2)),其中T是的类型E1,不同的是E1只计算一次.

因此,对于表达式array[i++%size] += i + " ";,该部件array[i++%size]应仅被评估一次.但它被评估两次(一次用于加载,一次用于商店).

所以是的,这是一个错误.


一些更新:

该错误在JDK 11中得到修复,并且将有一个JDK 10后端口(但不是JDK 9,因为它不再接收公共更新).

Aleksey Shipilev在JBS页面上提及(和评论中的@DidierL):

解决方法:使用编译 -XDstringConcat=inline

这将恢复使用StringBuilder连接,并没有错误.

  • 不重要的是,无论如何行为都被严重破坏了,但是第一次评估是针对存储而第二次针对负载,所以`array [index ++] + ="x";`将从`array [index + 1]读取`并写入`array [index]`... (43认同)
  • 顺便说一下,这适用于整个左手侧表达,而不仅仅是索引提供子表达式.该表达式可以是任意复杂的.例如,参见`IntStream.range(0,10).peek(System.out :: println).boxed().toArray()[0] + ="";`... (33认同)
  • 在JDK-8204322上,Aleksey Shipilev建议使用`-XDstringConcat = inline`作为解决方法进行编译,以满足那些需要它的人. (15认同)
  • @Holger左手边甚至不需要涉及数组,问题也出现在一个简单的`test().field + ="sth"`. (9认同)
  • @TheCoder是的,我想是的.JDK 9不是长期支持(LTS)版本.JDK 8是,而下一个LTS版本是JDK 11.请参见:http://www.oracle.com/technetwork/java/javase/eol-135779.html请注意,JDK 9的公开更新于3月结束. (5认同)
  • @DidierL [的确可以确认OlivierGrégoire_的_ @在TIO的Java 10所提供的代码编译器标志作品(https://tio.run/##TY2xTsMwEIb3PMUpElJCqFXERsiA6NqpC1KVwaROdME5R/alqEJ5dYwbt5Tp7P//7rteHuWqP3x6j8NoLEMfAjExanFfJsk4fWhsoNHSOdhKJPhOAC6pY8lhHA0eYAhdtmOL1O1rkLZz@YICIDEQVPBULt9/jJWnkJP6uoZUR@b1XDnRotbZgj1Amuaxa43NzkoMq@syjJcKHtfhdb0H0bzHorijGooqMAWkkMb9@ c8Sj4LSalDEz3HtZtmdHKtBmInFGEDWlF3Q/Gaak9n7n6bVsnN @ 9b5xi/PNUCO5QtJI6hc). (3认同)
  • @DidierL我知道.我已经测试过了.我只是没有找到适合注释的简单示例,因为没有JRE类具有适当的公共字段.我已经写了一个评论,一般指的是`+ =`(如果它是一个字符串连接),但显然,由于一个讽刺的附加句子,它已被删除.顺便说一下,该字段甚至可能是"静态",使冗余评估更不必要. (2认同)
  • @Holger哇,这真的很糟糕,我很惊讶没有看过这个,没有测试. (2认同)