Avatar ·

Почему array[idx++] += "a" увеличивает idx один раз в Java 8 и дважды в Java 9+?

Всем привет! Я столкнулся со странным поведением Java при использовании составного оператора присваивания с постфиксным инкрементом в массиве. У меня есть простой код: ```java public class Test { public static void main(String[] args) { String[] array = {"x", "y", "z"}; int idx = 0; array[idx++] += "a"; System.out.println("idx = " + idx); System.out.println("array[0] = " + array[0]); System.out.println("array[1] = " + array[1]); } } ``` В Java 8 этот код выводит: ``` idx = 1 array[0] = xa array[1] = y ``` То есть idx увеличивается один раз, и строка "a" добавляется к array[0]. Но в Java 9 и 10 я получаю: ``` idx = 2 array[0] = x array[1] = ya ``` То есть idx увеличивается дважды, и "a" добавляется к array[1]! Я понимаю, что это связано с изменениями в компиляторе javac между версиями. Но почему так происходит? Это баг или фича? Как правильно писать такой код, чтобы он работал одинаково во всех версиях Java? Помогите разобраться, пожалуйста! <<>>

Причина разного поведения в Java 8 и Java 9+

Дело в том, что составной оператор присваивания += в Java — это не атомарная операция. Спецификация Java (JLS §15.26.2) определяет, что выражение E1 op= E2 эквивалентно E1 = (T) ((E1) op (E2)), где T — тип E1. Однако ключевой момент — выражение E1 вычисляется только один раз.

В Java 8 компилятор javac генерировал байт-код, который действительно вычислял array[idx++] один раз. Схематично это выглядело так:

1. Сохранить текущее значение idx (0)
2. Увеличить idx (теперь idx = 1)
3. Получить array[0] (используя сохранённое значение)
4. Создать новую строку "xa"
5. Записать "xa" в array[0]

В Java 9 оптимизировали компилятор, и теперь байт-код стал другим. Из-за особенностей реализации составного оператора для ссылочных типов (String — ссылочный тип) компилятор начал генерировать код, который вычисляет array[idx++] дважды: один раз для чтения, второй раз для записи.

Как это выглядит в байт-коде Java 9+

1. Вычислить array[idx] (idx = 0) — для чтения
2. Увеличить idx (idx = 1)
3. Создать новую строку "xa"
4. Снова вычислить array[idx] (idx = 1) — для записи
5. Записать "xa" в array[1]

Обратите внимание: на шаге 4 idx уже равен 1, поэтому запись происходит в array[1], а idx к этому моменту увеличился дважды (на шаге 2 и при повторном вычислении индекса на шаге 4).

Почему это произошло?

Изменение связано с тем, что в Java 9+ компилятор начал использовать более агрессивную оптимизацию для работы со строками через invokedynamic и конкатенацию строк. В процессе рефакторинга кодогенерации для составных операторов над ссылочными типами возникла эта регрессия. Официально это признано багом (JDK-8204322) и исправлено в Java 11.

Как писать переносимый код?

Лучшее решение — не использовать побочные эффекты внутри составных операторов с массивами. Вот безопасные альтернативы:

// Вариант 1: разделить операции
int tempIdx = idx++;
array[tempIdx] += "a";

// Вариант 2: явная конкатенация
array[idx] = array[idx] + "a";
idx++;

// Вариант 3: если idx используется только здесь
array[idx] += "a";
idx++;

Главный урок: избегайте выражений с побочными эффектами в индексах массивов при использовании составных операторов. Это делает код непредсказуемым и зависимым от версии компилятора.

<<>>

Дополнение: что говорит спецификация Java?

Формально, спецификация Java (JLS §15.7) гарантирует, что операнды операторов вычисляются слева направо. Для выражения array[idx++] += "a" это означает:

  1. Сначала вычисляется левая часть — array[idx++]
  2. Затем правая часть — "a"
  3. Выполняется операция +=

Проблема в том, что спецификация не гарантирует, что выражение array[idx++] будет вычислено ровно один раз. В JLS §15.26.2 сказано, что левая часть составного оператора присваивания может вычисляться несколько раз, если это необходимо для корректного выполнения.

В Java 8 компилятор вычислял его один раз, в Java 9-10 — дважды. Оба поведения формально соответствуют спецификации, хотя второе явно нежелательно. Начиная с Java 11 это поведение исправлено — теперь idx увеличивается один раз, как и должно быть по здравому смыслу.

Рекомендация: даже если вы используете Java 11+, всё равно стоит избегать таких конструкций. Они снижают читаемость кода и могут запутать других разработчиков. Код должен быть явным, а не полагаться на тонкости работы компилятора.