Pregunta ¿Por qué array [idx ++] + = "a" aumenta idx una vez en Java 8 pero dos veces en Java 9 y 10?


Para un desafío, un compañero código golfista  escribió el siguiente código:

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

Al ejecutar este código en Java 8, obtenemos el siguiente resultado:

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 

Al ejecutar este código en Java 10, obtenemos el siguiente resultado:

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 numeración está completamente desactivada usando Java 10. Entonces, ¿qué está pasando aquí? ¿Es un error en Java 10?

Seguimiento de los comentarios:

  • El problema aparece cuando se compila con Java 9 o posterior (lo encontramos en Java 10). Compilar este código en Java 8, y luego ejecutarlo en Java 9 o en cualquier versión posterior, incluido el acceso anticipado a Java 11, da el resultado esperado.
  • Este tipo de código no es estándar, pero es válido de acuerdo con la especificación. Fue encontrado por Kevin Cruijssen en un discusión en una reto de golf, de ahí el extraño caso de uso encontrado.
  • Didier L descubrió que el problema se puede reproducir con el código mucho más pequeño y comprensible:

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

    Resultado cuando se compila en Java 8:

    evaluated
    

    Resultado cuando se compila en Java 9 y 10:

    evaluated
    evaluated
    
  • El problema parece estar limitado al operador de asignación y concatenación de cadenas (+=) con una expresión con efecto (s) secundario (s) como el operando izquierdo, como en array[test()]+="a", array[ix++]+="a", test()[index]+="a", o test().field+="a". Para habilitar la concatenación de cadenas, al menos uno de los lados debe tener el tipo String. Tratar de reproducir esto en otros tipos o construcciones falló.


655
2018-06-04 15:16


origen


Respuestas:


Este es un error en javac a partir de JDK 9 (que realizó algunos cambios con respecto a la concatenación de cadenas, que sospecho que es parte del problema), como lo confirma el javac equipo bajo el bug id JDK-8204322. Si miras el bytecode correspondiente a la línea:

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

Es:

  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

Donde el último aaload es la carga real de la matriz. Sin embargo, la parte

  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

Que corresponde aproximadamente a la expresión array[i++%size] (menos la carga real y la tienda), está allí dos veces. Esto es incorrecto, como dice la especificación en jls-15.26.2:

Una expresión de asignación compuesta de la forma E1 op= E2 es equivalente a E1 = (T) ((E1) op (E2)), dónde T es el tipo de E1, excepto eso E1 es evaluado solo una vez

Entonces, para la expresión array[i++%size] += i + " ";, la parte array[i++%size] solo se debe evaluar una vez. Pero se evalúa dos veces (una para la carga y otra para la tienda).

Entonces sí, esto es un error.


Algunas actualizaciones:

El error está arreglado en JDK 11 y habrá una puerto de respaldo a JDK 10 (pero no JDK 9, ya que ya no recibe actualizaciones públicas)

Aleksey Shipilev menciona en el Página JBS (y @DidierL en los comentarios aquí):

Solución alternativa: compilar con -XDstringConcat=inline

Eso volverá a usar StringBuilder para hacer la concatenación, y no tiene el error.


572
2018-06-04 18:26