Каковы правила для порядка оценки в 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 (и С#): "подвыражения вычисляются слева направо". Поскольку A() отображается слева от C(), A() вычисляется первым, независимо от того, что C() участвует в умножении, а A() участвует только в сложении.

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

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

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

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

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

Так что же происходит?

  • A a[b] находится слева от b=0, поэтому сначала запускается a[b], что приводит к a[1]. Однако проверка правильности этой операции индексации задерживается.
  • Тогда происходит b=0.
  • Затем происходит проверка того, что a является действительным и a[1] находится в диапазоне
  • Присвоение значения a[1] происходит последним.

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

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

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

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

Они о С#, но большинство этих вещей одинаково хорошо применимы и к Java.

Ответ 2

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

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

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

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

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

C и С++ определенно отличаются от Java в этой области: их определения языка оставляют порядок оценки undefined преднамеренно, чтобы обеспечить большую оптимизацию. С# похож на 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;

который объясняет результат.

Ответ 5

Вот еще один пример, чтобы описать порядок приоритета в Java

Рассмотрим следующий пример кода:

class A{

    public static void main(String[] args){

    int x = 20;

    x =x+(x=5);

    System.out.println(x);

}}

Вопрос: Каков вывод x? Попробуйте решить эту проблему, прежде чем продолжить ответ.

Ответ:

Шаг 1: компилятор выделяет 20 переменной x. Это важно - все переменные определяются сначала i.e.

int x = 20;

Шаг 2. Компилятор применяет правила порядка приоритета, то есть скобки сначала:

(x=5);

Теперь мы имеем:

x =20+(x=5);

Результат:

x =20+5;

Следовательно, выход равен 25:

System.out.println(x);

Выводы:

Необходимо, чтобы вы были такими:

1 Все переменные определяются сначала в выражениях.

2 Вы знаете оператор порядка приоритета и ассоциативности.

Ответ 6

Рассмотрим еще один более подробный пример ниже.

Как общее правило большого пальца:

Лучше всего иметь таблицу Правил Приоритета и Ассоциативность, доступную для чтения при решении этих вопросов, например. http://introcs.cs.princeton.edu/java/11precedence/

Вот хороший пример:

System.out.println(3+100/10*2-13);

Вопрос: Каков вывод вышеуказанной строки?

Ответ: применить правила приоритета и ассоциативности

Шаг 1: Согласно правилам приоритета:/и * операторы имеют приоритет над + - операторами. Поэтому начальная точка для выполнения этого уравнения будет сужаться до:

100/10*2

Шаг 2: Согласно правилам и приоритету:/и * равны в приоритете.

Операторы/и * равны в приоритете, нам нужно посмотреть на ассоциативность между этими операторами.

В соответствии с ПРАВИЛАМИ ASSOCIATIVITY этих двух конкретных операторов, мы начинаем выполнение уравнения от LEFT TO RIGHT, то есть 100/10 выполняется сначала:

100/10*2
=100/10
=10*2
=20

Шаг 3: Уравнение теперь находится в следующем состоянии выполнения:

=3+20-13

В соответствии с правилами и приоритетом: + и - равны в приоритете.

Теперь нам нужно посмотреть на ассоциативность между операторами + и - операторами. Согласно ассоциативности этих двух конкретных операторов, мы начинаем выполнение уравнения от LEFT до RIGHT, т.е. сначала выполняется 3 + 20:

=3+20
=23
=23-13
=10

10 - правильный вывод при компиляции

Опять же, важно иметь с собой таблицу Правил Приоритета и Ассоциативности при решении этих вопросов, например. http://introcs.cs.princeton.edu/java/11precedence/

Ответ 7

public class TestClass{    
      public static void main(String args[] ){       
          int i = 0 ;
          int[] iA = {10, 20} ;
          iA[i] = i = 30 ;
          System.out.println(""+ iA[ 0 ] + " " + iA[ 1 ] + "  "+i) ;     
 } }

Он напечатает 30 20 30

Утверждение iA [i] = я = 30; будет обрабатываться следующим образом:

iA [i] = я = 30; = > iA [0] = я = 30; = > я = 30; iA [0] = i; = > iA [0] = 30;

Вот что говорит JLS об этом:

1 Оцените левую руку Операнда сначала
2 Оцените операнды до операции
3 Оценка соответствия скобкам и приоритетам
4 Списки аргументов оцениваются влево-вправо

Для массивов: во-первых, выражения измерения оцениваются слева направо. Если какая-либо из оценок выражения завершается внезапно, выражения, находящиеся справа от нее, не оцениваются.