Warum erhöht Array [idx ++] + = "a" idx einmal in Java 8, aber zweimal in Java 9 und 10?

751

Für eine Herausforderung hat ein anderer Code-Golfer den folgenden Code geschrieben :

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

Wenn Sie diesen Code in Java 8 ausführen, erhalten Sie das folgende Ergebnis:

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 

Wenn Sie diesen Code in Java 10 ausführen, erhalten Sie das folgende Ergebnis:

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 

Die Nummerierung ist mit Java 10 völlig deaktiviert. Was passiert also hier? Ist es ein Fehler in Java 10?

Follow-ups aus den Kommentaren:

  • Das Problem tritt auf, wenn es mit Java 9 oder höher kompiliert wird (wir haben es in Java 10 gefunden). Wenn Sie diesen Code unter Java 8 kompilieren und dann in Java 9 oder einer späteren Version ausführen, einschließlich des frühen Zugriffs auf Java 11, erhalten Sie das erwartete Ergebnis.
  • Diese Art von Code ist nicht Standard, aber gemäß der Spezifikation gültig. Es wurde von Kevin Cruijssen in einer Diskussion in einer Golf-Herausforderung gefunden , daher der seltsame Anwendungsfall.
  • Didier L fand heraus, dass das Problem mit dem viel kleineren und verständlicheren Code reproduziert werden kann:

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

    Ergebnis beim Kompilieren in Java 8:

    evaluated

    Ergebnis beim Kompilieren in Java 9 und 10:

    evaluated
    evaluated
    
  • Das Problem scheint mit der String - Verkettung und Zuweisungsoperator (zu beschränken +=) mit einem Ausdruck mit Nebenwirkung (en) als der linken Operand, wie in array[test()]+="a", array[ix++]+="a", test()[index]+="a"oder test().field+="a". Um die Verkettung von Zeichenfolgen zu aktivieren, muss mindestens eine der Seiten vom Typ sein String. Der Versuch, dies auf anderen Typen oder Konstrukten zu reproduzieren, schlug fehl.

Olivier Grégoire
quelle
5
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew
13
@JollyJoker Es ist auf +=indirekte StringVerweise beschränkt. Ihr Array muss also zuerst a sein String[]. Das Problem tritt nicht mit int[], long[]und Freunde. Aber ja, du hast im Grunde recht!
Olivier Grégoire
2
@ OlivierGrégoire das Array muss nicht sein String[]. Wenn es so ist Object[]und du es tust array[expression] += "foo";, ist es dasselbe. Aber ja, es gilt nicht für primitive Arrays, wie es in der Lage sein muss , Referenzen vom Typ zu halten String( Object[], CharSequence[], Comparable[], ...) zu speichern , das Ergebnis der String - Verkettung.
Holger
30
Diesem wurde die Fehler-ID JDK-8204322 zugewiesen .
Stuart Marks
1
@StuartMarks danke! Das wurde in die Antwort integriert: Ich wollte die Frage wirklich als Frage behalten, ob es normal ist oder ein Fehler. Wir könnten jedoch die ID des Fehlers in der Antwort genauer angeben. Ich werde es jetzt anpassen.
Olivier Grégoire

Antworten:

625

Dies ist ein Fehler beim javacStarten von JDK 9 (der einige Änderungen in Bezug auf die Verkettung von Zeichenfolgen vorgenommen hat, von denen ich vermute, dass sie Teil des Problems sind), wie vom javacTeam unter der Fehler-ID JDK-8204322 bestätigt . Wenn Sie sich den entsprechenden Bytecode für die Zeile ansehen:

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

Es ist:

  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

Wobei die letzte aaloaddie tatsächliche Last aus dem Array ist. Allerdings ist das Teil

  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

Was in etwa dem Ausdruck entspricht array[i++%size](abzüglich der tatsächlichen Last und Speicherung), ist dort zweimal enthalten. Dies ist falsch, wie in der Spezifikation in jls-15.26.2 angegeben :

Eine Verbindung Zuweisungsausdruck von der Form E1 op= E2entspricht E1 = (T) ((E1) op (E2)), in dem Tdie Art der E1, außer dass E1nur einmal ausgewertet wird.

Für den Ausdruck array[i++%size] += i + " ";sollte der Teil also array[i++%size]nur einmal ausgewertet werden. Es wird jedoch zweimal ausgewertet (einmal für die Ladung und einmal für das Geschäft).

Also ja, das ist ein Fehler.


Einige Updates:

Der Fehler wurde in JDK 11 behoben und es wird einen Back-Port zu JDK 10 geben (jedoch nicht zu JDK 9, da keine öffentlichen Updates mehr empfangen werden ).

Aleksey Shipilev erwähnt auf der JBS-Seite (und @DidierL in den Kommentaren hier):

Problemumgehung: Kompilieren mit -XDstringConcat=inline

Das wird wieder verwendet StringBuilder, um die Verkettung durchzuführen, und hat den Fehler nicht.

Jorn Vernee
quelle
34
Dies gilt übrigens für den gesamten Ausdruck auf der linken Seite, nicht nur für den Index, der den Unterausdruck bereitstellt. Dieser Ausdruck kann beliebig komplex sein. Siehe zum Beispiel IntStream.range(0, 10) .peek(System.out::println).boxed().toArray()[0] += "";
Holger
9
@Holger Die linke Seite muss nicht einmal Arrays beinhalten, das Problem tritt auch bei einem einfachen auf test().field += "sth".
Didier L
44
Nicht, dass es wichtig wäre, das Verhalten ist sowieso schrecklich kaputt, aber die erste Bewertung ist für den Laden und die zweite für die Ladung, also array[index++] += "x";wird aus gelesen array[index+1]und an array[index]...
Holger
5
@ TheCoder Ja, ich denke schon. JDK 9 ist keine LTS-Version (Long Term Support). JDK 8 war und die nächste LTS-Version ist JDK 11. Siehe hier: oracle.com/technetwork/java/javase/eol-135779.html Beachten Sie, dass öffentliche Updates für JDK 9 im März endeten.
Jorn Vernee
15
Auf JDK-8204322 schlug Aleksey Shipilev vor, -XDstringConcat=inlineals Problemumgehung für diejenigen zu kompilieren , die es benötigen.
Didier L