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

Для вызова товарищ по гольфунаписал следующий код:

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. Попытка воспроизвести это на других типах или конструкциях не удалась.

Комментарии не подлежат расширенному обсуждению; этот разговор был переехал в чат.

Samuel Liew 05.06.2018 01:22

Похоже, что это легко может быть проблемой в «реальном» коде. Если я правильно понял, чего-то вроде array[i++] += n достаточно, чтобы дважды запустить i++?

JollyJoker 05.06.2018 10:08

@JollyJoker Ограничено +=, применяемым к косвенным ссылкам на String. Итак, сначала ваш массив должен быть String[]. Проблема не возникает с int[], long[] и другими. Но да, вы в принципе правы!

Olivier Grégoire 05.06.2018 10:11

@ OlivierGrégoire массив не обязательно должен быть String[]. Если это Object[], а вы - array[expression] += "foo";, то это то же самое. Но да, это не относится к примитивным массивам, так как он должен иметь возможность хранить ссылки типа String (Object[], CharSequence[], Comparable[],…), чтобы сохранить результат конкатенации строк.

Holger 05.06.2018 11:29

Почему я не могу воспроизвести это в jdoodle? jdoodle.com/a/wSU

danadam 05.06.2018 16:09

@danadam Поскольку код скомпилирован с виртуальной машиной Java 8 или чем-то подобным: ключевое слово Java-10 var не распознается (например, в var x = "";.

Olivier Grégoire 05.06.2018 16:17

Этому был присвоен идентификатор ошибки JDK-8204322.

Stuart Marks 05.06.2018 17:19

@StuartMarks, спасибо! Это было интегрировано в ответ: я действительно хотел, чтобы вопрос оставался вопросом о том, нормально это или ошибка. Тем не менее, мы могли бы более четко указать идентификатор ошибки в ответе. Я его щас адаптирую.

Olivier Grégoire 05.06.2018 17:55

Что вы имеете в виду под «нестандартным» кодом?

xehpuk 08.06.2018 17:25

Операторы @xehpuk с несколькими эффектами / побочными эффектами.

Olivier Grégoire 08.06.2018 17:29

Подобные вещи заставляют меня поверить в то, что «Oracle не может должным образом обрабатывать Java-разработку»

nomadSK25 04.06.2019 12:42

Очень интересный баг, спасибо. Мы уже исправили это в JDK11

Vicente Romero 06.06.2018 22:56

Спасибо @VicenteRomero за то, что так быстро исправили эту ошибку! Когда будет публично доступен бэкпорт для Java 10?

Olivier Grégoire 07.06.2018 00:07

@ OlivierGrégoire, извините за поздний ответ, просто чтобы завершить обсуждение, проблема была перенесена в JDK 10 06.06.2018 bugs.openjdk.java.net/browse/JDK-8204873

Vicente Romero 26.02.2021 23:08
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
770
14
72 550
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это ошибка в 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:

A compound assignment expression of the form E1 op= E2 is equivalent to E1 = (T) ((E1) op (E2)), where T is the type of E1, except that E1 is evaluated only once.

Итак, для выражения array[i++%size] += i + " "; часть array[i++%size] должна оцениваться только один раз. Но он оценивается дважды (один раз для загрузки и один раз для магазина).

Так что да, это ошибка.


Некоторые обновления:

Ошибка исправлена ​​в JDK 11 и была перенесена на JDK 10 (здесь и здесь), но не на JDK 9, поскольку он больше не получает общедоступные обновления.

Алексей Шипилев упоминает о Страница JBS (и @DidierL в комментариях здесь):

Workaround: compile with -XDstringConcat=inline

Это вернется к использованию StringBuilder для выполнения конкатенации и не содержит ошибки.

Между прочим, это относится ко всему выражению с левой стороны, а не только к подвыражению, обеспечивающему индекс. Это выражение может быть сколь угодно сложным. См. Например IntStream.range(0, 10) .peek(System.out::println).boxed().toArray()[0] += "";

Holger 05.06.2018 11:45

@Holger Левая часть даже не требует использования массивов, проблема также возникает с простым test().field += "sth".

Didier L 05.06.2018 13:40

@DidierL Я знаю. Я это уже проверял. Я просто не нашел простого примера, подходящего для комментария, так как не существует класса JRE с подходящим общедоступным полем. Я уже писал комментарий, относящийся к += в целом (если это конкатенация строк), но, видимо, он был удален из-за саркастического дополнительного предложения. Кстати, это поле может быть даже static, что делает избыточную оценку еще менее ненужной.

Holger 05.06.2018 13:46

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

Holger 05.06.2018 14:08

Согласно отчету об ошибке, версия прямого исправления - 11, и она будет перенесена на 10. Значит, для 9 не будет никаких резервных копий?

The Coder 06.06.2018 09:47

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

Jorn Vernee 06.06.2018 10:37

@Holger вау, это действительно плохо, я удивлен, что никто не видел этого раньше и не было тестов.

agilob 06.06.2018 13:44

Не то, чтобы жаловаться. Если исправление не будет перенесено в JDK 9, то практически никто не должен запускать свои приложения JDK 9 :(

The Coder 06.06.2018 16:40

@TheCoder, если вы обновились до Java 9, вам нужно было обновиться до Java 10, как только она вышла, так как она подписала конец общедоступных обновлений для Java 9. То же самое будет с 10 и 11. 11 будет LTS, поэтому это должно быть довольно типично прыгать с 8 на 11.

Didier L 06.06.2018 21:24

На JDK-8204322 Алексей Шипилев предложил скомпилировать с -XDstringConcat=inline в качестве обходного пути для тех, кому это нужно.

Didier L 06.06.2018 21:50

Другие вопросы по теме