Pourquoi le tableau [idx ++] + = “a” augmente idx une fois en Java 8 mais deux fois en Java 9 et 10?

751

Pour un défi, un autre golfeur de code a écrit le code suivant :

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

Lors de l'exécution de ce code dans Java 8, nous obtenons le résultat suivant:

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 

Lors de l'exécution de ce code dans Java 10, nous obtenons le résultat suivant:

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 

La numérotation est entièrement désactivée en utilisant Java 10. Alors, que se passe-t-il ici? Est-ce un bug dans Java 10?

Suivi des commentaires:

  • Le problème apparaît lors de la compilation avec Java 9 ou version ultérieure (nous l'avons trouvé dans Java 10). La compilation de ce code sur Java 8, puis son exécution dans Java 9 ou toute version ultérieure, y compris l'accès anticipé Java 11, donne le résultat attendu.
  • Ce type de code n'est pas standard, mais est valide selon les spécifications. Il a été trouvé par Kevin Cruijssen lors d'une discussion dans un défi de golf , d'où le cas d'utilisation étrange rencontré.
  • Didier L a découvert que le problème peut être reproduit avec le code beaucoup plus petit et plus compréhensible:

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

    Résultat lors de la compilation en Java 8:

    evaluated

    Résultat lors de la compilation en Java 9 et 10:

    evaluated
    evaluated
  • La question semble se limiter à la concaténation de chaîne et l' opérateur d'affectation ( +=) avec une expression avec effet secondaire (s) comme l'opérande gauche, comme dans array[test()]+="a", array[ix++]+="a", test()[index]+="a"ou test().field+="a". Pour activer la concaténation de chaînes, au moins un des côtés doit avoir un type String. Échec de la tentative de reproduction de cela sur d'autres types ou constructions.

Olivier Grégoire
la source
5
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew
13
@JollyJoker Il est limité à +=appliqué aux Stringréférences indirectes . Donc, tout d'abord, votre tableau doit être un String[]. Le problème ne se produit pas int[], long[]et les amis. Mais oui, vous avez fondamentalement raison!
Olivier Grégoire
2
@ OlivierGrégoire le tableau n'a pas besoin d'être String[]. Si c'est le cas Object[]et que vous le faites array[expression] += "foo";, c'est la même chose. Mais oui, il ne concerne pas les tableaux primitifs, car il doit être capable de contenir des références de type String( Object[], CharSequence[], Comparable[], ...), pour stocker le résultat de la concaténation de chaîne.
Holger
30
Il a été attribué l'ID de bogue JDK-8204322 .
Stuart marque le
1
@StuartMarks merci! Cela a été intégré dans la réponse: je voulais vraiment garder la question une question, si c'est normal ou un bug. Cependant, nous pourrions être plus explicites sur l'ID du bogue dans la réponse. Je vais l'adapter tout de suite.
Olivier Grégoire

Réponses:

625

Il s'agit d'un bogue à javacpartir de JDK 9 (qui a apporté quelques modifications en ce qui concerne la concaténation de chaînes, ce qui, je pense, fait partie du problème), comme l'a confirmé l' javacéquipe sous l'ID de bogue JDK-8204322 . Si vous regardez le bytecode correspondant pour la ligne:

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

C'est:

  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

Où le dernier aaloadest la charge réelle de la baie. Cependant, la partie

  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

Ce qui correspond à peu près à l'expression array[i++%size](moins la charge et le stockage réels), est là deux fois. C'est incorrect, comme le dit la spécification dans jls-15.26.2 :

Une expression d'affectation composée du formulaire E1 op= E2est équivalente à E1 = (T) ((E1) op (E2)), où Test le type de E1, sauf qu'elle E1n'est évaluée qu'une seule fois.

Ainsi, pour l'expression array[i++%size] += i + " ";, la pièce array[i++%size]ne doit être évaluée qu'une seule fois. Mais il est évalué deux fois (une fois pour la charge et une fois pour le magasin).

Alors oui, c'est un bug.


Quelques mises à jour:

Le bug est corrigé dans JDK 11 et il y aura un back-port vers JDK 10 (mais pas JDK 9, car il ne reçoit plus de mises à jour publiques ).

Aleksey Shipilev mentionne sur la page JBS (et @DidierL dans les commentaires ici):

Solution: compilez avec -XDstringConcat=inline

Cela reviendra à utiliser StringBuilderpour faire la concaténation, et n'a pas le bogue.

Jorn Vernee
la source
34
Soit dit en passant, cela s'applique à toute l'expression de gauche, pas seulement à l'index fournissant la sous-expression. Cette expression peut être arbitrairement complexe. Voir par exemple IntStream.range(0, 10) .peek(System.out::println).boxed().toArray()[0] += "";
Holger
9
@Holger Le côté gauche n'a même pas besoin d'impliquer des tableaux, le problème se produit également avec un simple test().field += "sth".
Didier L
44
Ce n'est pas important, le comportement est horriblement cassé de toute façon, mais la première évaluation est pour le magasin et la seconde pour la charge, alors array[index++] += "x";je vais lire array[index+1]et écrire à array[index]
Holger
5
@TheCoder Oui, je pense que oui. JDK 9 n'est pas une version de support à long terme (LTS). JDK 8 l'était, et la prochaine version de LTS est JDK 11. Voir ici: oracle.com/technetwork/java/javase/eol-135779.html Notez que les mises à jour publiques de JDK 9 ont pris fin en mars.
Jorn Vernee
15
Le JDK-8204322, Aleksey Shipilev a suggéré de compiler avec -XDstringConcat=inlinecomme solution de contournement, pour ceux qui en ont besoin.
Didier L