Вопрос-ответ

What are the rules for evaluation order in Java?

Каковы правила для порядка вычисления в Java?

Я читаю некоторый текст на Java и получаю следующий код:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;

В тексте автор не дал четкого объяснения, и эффект последней строки таков: a[1] = 0;

Я не уверен, что понимаю: как произошла оценка?

Переведено автоматически
Ответ 1

Позвольте мне сказать это очень четко, потому что люди все время неправильно понимают это:

Порядок вычисления подвыражений независим как от ассоциативности, так и от приоритета. Ассоциативность и приоритет определяют, в каком порядке выполняются операторы, но не определяют, в каком порядке вычисляются подвыражения. Ваш вопрос касается порядка, в котором вычисляются подвыражения.

Рассмотрим A() + B() + C() * D(). Умножение имеет более высокий приоритет, чем сложение, а сложение является левоассоциативным, так что это эквивалентно (A() + B()) + (C() * D()) Но знание этого говорит вам только о том, что первое сложение произойдет до второго сложения и что умножение произойдет до второго сложения. Это не говорит вам, в каком порядке будут вызываться A(), B(), C () и D ()! (Он также не сообщает вам, происходит ли умножение до или после первого сложения.) Было бы вполне возможно подчиниться правилам приоритета и ассоциативности, скомпилировав это как:

d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

Здесь соблюдаются все правила приоритета и ассоциативности - первое сложение происходит перед вторым сложением, а умножение происходит перед вторым сложением. Очевидно, что мы можем выполнять вызовы A(), B(), C() и D() в любом порядке и при этом подчиняться правилам приоритета и ассоциативности!

Нам нужно правило, не связанное с правилами приоритета и ассоциативности, чтобы объяснить порядок, в котором вычисляются подвыражения. Соответствующее правило в Java (и C #) гласит: "подвыражения вычисляются слева направо". Поскольку A() отображается слева от C(), A() вычисляется первым, независимо от того факта, что C() участвует в умножении, а A() - только в сложении.

Итак, теперь у вас достаточно информации, чтобы ответить на ваш вопрос. В a[b] = b = 0 правилах ассоциативности сказано, что это a[b] = (b = 0); но это не значит, что b=0 выполняется первым! Правила приоритета гласят, что индексация имеет более высокий приоритет, чем присвоение, но это не означает, что индексатор запускается перед самым правым присвоением.

(ОБНОВЛЕНИЕ: в более ранней версии этого ответа были некоторые небольшие и практически несущественные упущения в следующем разделе, которые я исправил. Я также написал статью в блоге, описывающую, почему эти правила применимы в Java и C #, здесь: https://ericlippert.com/2019/01/18/indexer-error-cases / )

Приоритет и ассоциативность говорят нам только о том, что присвоение нуля b должно произойти перед присвоением a[b], потому что присвоение нуля вычисляет значение, которое присваивается в операции индексации. Приоритет и ассоциативность сами по себе ничего не говорят о том, a[b] вычисляется до или после b=0.

Опять же, это то же самое, что: A()[B()] = C() - Все, что мы знаем, это то, что индексация должна происходить перед назначением. Мы не знаем, выполняется ли A(), B () или C () первым на основе приоритета и ассоциативности. Нам нужно другое правило, которое сообщит нам об этом.

Правило, опять же, гласит: "когда у вас есть выбор, что делать в первую очередь, всегда двигайтесь слева направо". Однако в этом конкретном сценарии есть интересная загвоздка. Считается ли побочный эффект генерируемого исключения, вызванного нулевой коллекцией или индексом вне диапазона, частью вычисления левой части присваивания или частью вычисления самого присваивания? Java выбирает последнее. (Конечно, это различие имеет значение только если код уже неправильный, потому что правильный код изначально не разыменовывает null и не передает неверный индекс.)

Итак, что происходит?


  • a[b] Находится слева от b=0, поэтому a[b] выполняется первым, что приводит к a[1]. Однако проверка валидности этой операции индексирования откладывается.

  • Затем происходит b=0.

  • Затем выполняется проверка того, что a является допустимым и a[1] находится в диапазоне

  • Присвоение значения a[1] происходит последним.

Итак, хотя в этом конкретном случае необходимо учитывать некоторые тонкости для тех редких случаев ошибок, которые изначально не должны возникать в правильном коде, в целом вы можете рассуждать так: события слева происходят раньше событий справа. Это то правило, которое вы ищете. Разговоры о приоритете и ассоциативности сбивают с толку и неуместны.

Люди все время ошибаются в этом вопросе, даже те, кто должен знать лучше. Я отредактировал слишком много книг по программированию, в которых неверно изложены правила, поэтому неудивительно, что у многих людей совершенно неверные представления о взаимосвязи между приоритетом / ассоциативностью и порядком вычисления, а именно, что на самом деле такой взаимосвязи нет; они независимы.

Если эта тема вас заинтересует, ознакомьтесь с моими статьями на эту тему для дальнейшего чтения:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

Они касаются C #, но большая часть этого материала одинаково хорошо применима и к Java.

Ответ 2

Мастерский ответ Эрика Липперта, тем не менее, не является должным образом полезным, потому что в нем говорится о другом языке. Это Java, где спецификация языка Java является окончательным описанием семантики. В частности, § 15.26.1 актуален, потому что он описывает порядок вычисления для = оператора (мы все знаем, что он ассоциативный, да?). Немного сокращаем его до тех частей, которые нас интересуют в этом вопросе:


Если выражение левого операнда является выражением доступа к массиву (§ 15.13), то требуется выполнить много шагов:



  • Сначала вычисляется подвыражение ссылки на массив выражения доступа к массиву левого операнда. Если это вычисление завершается внезапно, то выражение присваивания завершается внезапно по той же причине; подвыражение индекса (выражения доступа к массиву левых операндов) и правый операнд не вычисляются, и присвоение не происходит.

  • В противном случае вычисляется подвыражение индекса выражения доступа к массиву левых операндов. Если это вычисление завершается внезапно, то выражение присваивания завершается внезапно по той же причине, и правый операнд не вычисляется, и присвоение не происходит.

  • В противном случае вычисляется правый операнд. Если это вычисление завершается внезапно, то выражение присваивания завершается внезапно по той же причине, и присваивание не происходит.


[... затем далее описывается фактическое значение самого присваивания, которое мы можем здесь проигнорировать для краткости ...]

Короче говоря, Java имеет очень четко определенный порядок вычисления, который практически точно располагается слева направо в аргументах любого оператора или вызова метода. Назначение массива - один из более сложных случаев, но даже там это все еще L2R. (JLS рекомендует вам не писать код, который нуждается в такого рода сложных семантических ограничениях, и я тоже: у вас может возникнуть более чем достаточно проблем с одним присваиванием для каждого оператора!)

C и C ++ определенно отличаются от Java в этой области: их языковые определения намеренно оставляют порядок вычисления неопределенным, чтобы обеспечить дополнительную оптимизацию. C #, по-видимому, похож на Java, но я недостаточно хорошо знаю его литературу, чтобы иметь возможность указать на формальное определение. (Хотя это действительно зависит от языка, Ruby строго L2R, как и Tcl — хотя в нем отсутствует оператор присваивания как таковой по причинам, здесь не относящимся к делу, — и Python - это L2R, но R2L в отношении присваивания, что я нахожу странным, но так и есть.)

Ответ 3
a[b] = b = 0;

1) оператор индексации массива имеет более высокий приоритет, чем оператор присваивания (см. Этот ответ):

(a[b]) = b = 0;

2) Согласно 15.26. Операторы присваивания в JLS


Существует 12 операторов присваивания; все синтаксически правоассоциативны (они группируются справа налево). Таким образом, a= b = c означает a=(b = c), который присваивает значение c b, а затем присваивает значение b a.


(a[b]) = (b=0);

3) Согласно 15.7. Порядок вычисления в JLS


Язык программирования Java гарантирует, что операнды операторов будут оцениваться в определенном порядке вычисления, а именно слева направо.


и


Левый операнд двоичного оператора, по-видимому, полностью вычисляется до того, как будет вычислена какая-либо часть правого операнда.


Итак:

a) (a[b]) сначала выполняется вычисление для a[1]

b) затем (b=0) вычисляется до 0

c) (a[1] = 0) вычисляется последним

Ответ 4

Ваш код эквивалентен:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;

что объясняет результат.

java