Чому масив [idx ++] + = “a” збільшує idx один раз у Java 8, але двічі в Java 9 та 10?


751

Для виклику, гольфіст з коду написав такий код :

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

Запускаючи цей код у 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 

Запускаючи цей код на 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 

Нумерація повністю виключена за допомогою Java 10. Отже, що тут відбувається? Це помилка в Java 10?

Слідкуйте за коментарями:

  • Проблема з’являється при компілюванні з Java 9 або пізнішої версії (ми знайшли її в Java 10). Складання цього коду на Java 8, а потім запуск у Java 9 або будь-якій пізнішій версії, включаючи ранній доступ до Java 11, дає очікуваний результат.
  • Цей вид коду нестандартний, але дійсний відповідно до специфікації. Це було знайдено Кевіном Круйссеном під час дискусії в гольф-проблемі , отже, зустрічався дивний випадок використання.
  • Дідьє Л дізнався, що проблему можна відтворити за допомогою набагато меншого і зрозумілого коду:

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

    Результат при компіляції в Java 8:

    evaluated

    Результат при компіляції в Java 9 і 10:

    evaluated
    evaluated
  • Проблема , як видається, обмежується конкатенацией і оператор присвоювання ( +=) з виразом з побічним ефектом (и) в якості лівого операнда, як і в array[test()]+="a", array[ix++]+="a", test()[index]+="a", або test().field+="a". Щоб увімкнути об'єднання рядків, принаймні одна з сторін повинна мати тип String. Не вдалося спробувати відтворити це на інших типах або конструкціях.


5
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

13
@JollyJoker Це обмежено для +=застосування до непрямих Stringпосилань. Отже, спочатку у вашому масиві має бути а String[]. Проблема не виникає з int[], long[]і друзями. Але так, ви в основному праві!
Олів'є Грегоар

2
@ OlivierGrégoire масиву не потрібно String[]. Якщо це так Object[]і ви робите array[expression] += "foo";, це те саме. Але так, це не відноситься до примітивних масивів, так як він повинен бути в змозі зберігати посилання типу String( Object[], CharSequence[], Comparable[]...), щоб зберегти результат конкатенації.
Холгер

30
Цьому призначено ідентифікатор помилки JDK-8204322 .
Стюарт Маркс

1
@StuartMarks дякую! Це було інтегровано у відповідь: я дуже хотів, щоб питання було питанням, чи це нормально, чи помилка. Хоча ми могли б бути більш чіткими щодо ідентифікатора помилки у відповіді. Я зараз це адаптую.
Олів'є Грегоар

Відповіді:


625

Це помилка, javacпочинаючи з JDK 9 (яка внесла деякі зміни щодо конкатенації рядків, які, як я підозрюю, є частиною проблеми), що підтверджено javacкомандою під ідентифікатором помилки 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 + " ";частина array[i++%size]повинна бути оцінена лише один раз. Але вона оцінюється двічі (один раз для вантажу, і один раз для магазину).

Так так, це помилка.


Деякі оновлення:

Помилка виправлена ​​в JDK 11, і буде JDK 10 (але не JDK 9, оскільки він більше не отримує публічні оновлення ).

Олексій Шипілєв згадує на сторінці JBS (та @DidierL у коментарях тут):

Обхід: компілювати з -XDstringConcat=inline

Це повернеться до використання StringBuilderдля об'єднання, і не має помилок.


34
До речі, це стосується всього лівого виразу вираження, а не лише індексу, що забезпечує підвираз. Цей вираз може бути довільно складним. Дивіться для прикладу IntStream.range(0, 10) .peek(System.out::println).boxed().toArray()[0] += "";
Холгер

9
@Holger Лівій стороні навіть не потрібно включати масиви, проблема також виникає з простим test().field += "sth".
Дідьє Л

44
Не важливо, що поведінка в будь-якому випадку жахливо порушена, але перша оцінка для магазину, а друга для вантажу, тому array[index++] += "x";буде читати array[index+1]і писати до array[index]
Holger

5
@TheCoder Так, я так думаю. JDK 9 не є довгостроковою підтримкою (LTS). JDK 8 був, а наступний реліз LTS - JDK 11. Дивіться тут: oracle.com/technetwork/java/javase/eol-135779.html Зауважте, що публічні оновлення до JDK 9 закінчилися у березні.
Джорн Верні

15
На JDK-8204322 Олексій Шипілєв запропонував скласти, -XDstringConcat=inlineяк вирішення, для тих, хто цього потребує.
Дідьє Л
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.