Почему x == (x = y) не совпадает с (x = y) == x?

207

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

class Quirky {
    public static void main(String[] args) {
        int x = 1;
        int y = 3;

        System.out.println(x == (x = y)); // false
        x = 1; // reset
        System.out.println((x = y) == x); // true
     }
}

Я не уверен, есть ли в Спецификации языка Java элемент, который диктует загрузку предыдущего значения переменной для сравнения с правой стороной ( x = y), которая в порядке, подразумеваемом скобками, должна вычисляться первой.

Почему первое выражение оценивается false, а второе - true? Я ожидал, (x = y)что сначала будет вычислена оценка, а затем она сравнится xс самой собой ( 3) и вернется true.


Этот вопрос отличается от порядка вычисления подвыражений в выражении Java тем, что xздесь определенно не «подвыражение». Его нужно загружать для сравнения, а не «оценивать». Вопрос специфичен для Java, и выражение x == (x = y), в отличие от надуманных непрактичных конструкций, обычно создаваемых для сложных вопросов интервью, пришло из реального проекта. Предполагалось, что это будет замена одной строки для сравнения и замены

int oldX = x;
x = y;
return oldX == y;

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

Джон Макклейн
источник
62
Левая сторона всегда оценивается перед правой. Скобки не имеют значения к этому.
Луи Вассерман
11
Оценка выражения x = y, безусловно, актуальна и вызывает побочный эффект, для xкоторого установлено значение y.
Луи Вассерман
50
Сделайте себе и своим товарищам по команде услугу и не смешивайте мутацию состояния в одну строку с экзаменом штата. Это резко снижает читабельность вашего кода. (В некоторых случаях это абсолютно необходимо из-за требований атомарности, но функции для них уже существуют, и их назначение будет немедленно распознано.)
jpmc26
50
На самом деле вопрос в том, почему вы хотите написать такой код.
Klutt
26
Ключом к вашему вопросу является ваше ложное убеждение, что скобки подразумевают порядок оценки. Это общее убеждение из-за того, как нас учат математике в начальной школе, и из-за того, что некоторые книги по программированию для начинающих все еще ошибаются , но это ложное убеждение. Это довольно частый вопрос. Вам может быть полезно прочитать мои статьи на эту тему; они относятся к C #, но относятся к Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Эрик Липперт,

Ответы:

97

которые в порядке, указанном в скобках, должны быть рассчитаны в первую очередь

Нет. Распространенным заблуждением является то, что круглые скобки оказывают (общее) влияние на порядок вычисления или оценки. Они только приводят части вашего выражения в определенное дерево, связывая правильные операнды с правильными операциями для задания.

(И, если вы не используете их, эта информация поступает из «приоритета» и ассоциативности операторов, что является результатом того, как определяется синтаксическое дерево языка. На самом деле, это все равно именно так, как оно работает, когда вы используйте скобки, но мы упрощаем и говорим, что тогда мы не полагаемся ни на какие правила приоритета.)

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

Обратите внимание, что это не так на всех языках; например, в C ++, если вы не используете оператор короткого замыкания, такой как &&или ||, порядок вычисления операндов обычно не определен, и вы не должны полагаться на него в любом случае.

Учителя должны прекратить объяснять приоритет оператора, используя вводящие в заблуждение фразы, такие как «это делает сложение первым». Для данного выражения x * y + zправильным объяснением будет «операторский приоритет делает сложение между x * yи z, а не между yи z», без упоминания какого-либо «порядка».

Гонки легкости на орбите
источник
6
Я хотел бы, чтобы мои учителя провели некоторое разделение между основной математикой и синтаксисом, который они использовали для его представления, например, если бы мы провели день с римскими цифрами или польской нотацией или чем-то еще и увидели, что дополнение имеет те же свойства. Мы изучали ассоциативность и все эти свойства в средней школе, поэтому у нас было много времени.
Джон П
1
Рад, что вы упомянули, что это правило не распространяется на все языки. Кроме того, если какая-либо из сторон имеет другой побочный эффект, такой как запись в файл или чтение текущего времени, она (даже в Java) не определена в каком порядке это происходит. Однако результат сравнения будет таким, как если бы он оценивался слева направо (в Java). За исключением другого: довольно много языков просто запрещают смешивать присваивание и сравнение таким образом по правилам синтаксиса, и проблема не возникнет.
Авель
5
@JohnP: становится хуже. Означает ли 5 ​​* 4 5 + 5 + 5 + 5 или 4 + 4 + 4 + 4 + 4? Некоторые учителя настаивают на том, что только один из этих вариантов является правильным.
Брайан
3
@ Брайан Но ... но ... умножение действительных чисел коммутативно!
Гонки
2
В моем мире мышления пара скобок означает «необходимо для». Вычисляя «a * (b + c)», круглые скобки будут выражать, что результат сложения необходим для умножения. Любые неявные предпочтения оператора могут быть выражены паренами, кроме правил LHS-first или RHS-first. (Это правда?) @Brian В математике есть несколько редких случаев, когда умножение можно заменить повторным сложением, но это далеко не всегда верно (начиная с комплексных чисел, но не ограничиваясь ими). Таким образом, ваши преподаватели должны действительно следить за тем, что говорят люди ....
syck
164

==является оператором двоичного равенства .

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

Спецификация Java 11> Порядок оценки> Сначала оцените левый операнд

Андрей Тобилко
источник
42
Формулировка «кажется» не звучит так, как будто они уверены, что-то в этом роде.
Мистер Листер
86
«кажется» означает, что спецификация не требует, чтобы операции фактически выполнялись в этом порядке в хронологическом порядке, но требует, чтобы вы получили тот же результат, который вы получили бы, если бы они были.
Робин
24
@MrLister "кажется" кажется плохим выбором слов с их стороны. Под «появлением» они подразумевают «проявление как явление для разработчика». «эффективно» может быть лучшей фразой.
Кельвин
18
в сообществе C ++ это эквивалентно правилу «как будто» ... операнд должен вести себя «как если бы» он был реализован согласно следующим правилам, даже если технически это не так.
Майкл Иденфилд,
2
@ Кельвин Я согласен, я бы тоже выбрал это слово, а не «кажется».
MC Emperor
149

Как сказал Луис Вассерман, выражение оценивается слева направо. И java не заботится о том, что на самом деле делает «define», он заботится только о том, чтобы генерировать (энергонезависимое, окончательное) значение для работы.

//the example values
x = 1;
y = 3;

Таким образом, чтобы вычислить первый вывод System.out.println(), делается следующее:

x == (x = y)
1 == (x = y)
1 == (x = 3) //assign 3 to x, returns 3
1 == 3
false

и рассчитать второе:

(x = y) == x
(x = 3) == x //assign 3 to x, returns 3
3 == x
3 == 3
true

Обратите внимание, что второе значение всегда будет иметь значение true, независимо от начальных значений xи y, поскольку вы эффективно сравниваете присвоение значения переменной, которой оно назначено, и, a = bи, bбудет оцениваться в этом порядке, всегда будут одинаковыми по определению.

Poohl
источник
«Слева направо» также верно в математике, кстати, просто, когда вы получаете круглые скобки или приоритет, вы перебираете внутри них и оцениваете все внутри слева направо, прежде чем идти дальше по основному уровню. Но математика никогда бы этого не сделала; Различие имеет значение только потому, что это не уравнение, а комбинированная операция, выполняющая как присвоение, так и уравнение одним махом . Я бы никогда этого не сделал, потому что читаемость плохая, если бы я не занимался гольфом кода или не искал способ оптимизировать производительность, а затем были комментарии.
Харпер - Восстановить Монику
25

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

Там есть. В следующий раз, когда вам непонятно, что говорится в спецификации, прочитайте ее, а затем задайте вопрос, если она неясна.

... правая часть, (x = y)которая в порядке, указанном в скобках, должна быть рассчитана первой.

Это утверждение является ложным. Скобки не подразумевают порядок оценки . В Java порядок вычисления слева направо, независимо от круглых скобок. Скобки определяют, где находятся границы подвыражения, а не порядок оценки.

Почему первое выражение оценивается как ложное, а второе - как истинное?

Правило для ==оператора: оценить левую сторону для получения значения, оценить правую сторону для получения значения, сравнить значения, сравнение - это значение выражения.

Другими словами, значение expr1 == expr2всегда такое же, как если бы вы написали, temp1 = expr1; temp2 = expr2;а затем оценили temp1 == temp2.

Правило для =оператора с локальной переменной слева: вычисление левой стороны для создания переменной, вычисление правой стороны для получения значения, выполнение присваивания, результатом является присвоенное значение.

Итак, соберите это вместе:

x == (x = y)

У нас есть оператор сравнения. Оцените левую сторону, чтобы получить значение - мы получим текущее значение x. Оцените правую сторону: это присваивание, поэтому мы оцениваем левую сторону для создания переменной - переменной x- мы оцениваем правую сторону - текущее значение y- присваиваем ее x, а результатом является присвоенное значение. Затем мы сравниваем исходное значение xсо значением, которое было присвоено.

Вы можете сделать (x = y) == xв качестве упражнения. Опять же, помните, что все правила оценки левой стороны предшествуют всем правилам оценки правой стороны .

Я ожидал, что (x = y) будет сначала оценен, а затем он сравнил бы x с самим собой (3) и вернул бы true.

Ваше ожидание основано на ряде неверных представлений о правилах Java. Надеюсь, теперь у вас есть правильные убеждения и вы будете ожидать в будущем истинных вещей.

Этот вопрос отличается от «порядка вычисления подвыражений в выражении Java»

Это утверждение неверно. Этот вопрос совершенно уместен.

x здесь определенно не является «подвыражением».

Это утверждение также ложно. Это подвыражение дважды в каждом примере.

Его нужно загружать для сравнения, а не «оценивать».

Я без понятия что это значит.

Видимо у тебя еще много ложных убеждений. Мой совет, чтобы вы читали спецификацию, пока ваши ложные убеждения не будут заменены истинными убеждениями.

Вопрос специфичен для Java, и выражение x == (x = y), в отличие от надуманных непрактичных конструкций, обычно создаваемых для сложных вопросов интервью, пришло из реального проекта.

Происхождение выражения не имеет отношения к вопросу. Правила для таких выражений четко описаны в спецификации; прочитай это!

Предполагалось, что это будет замена одной строки для сравнения и замены

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

Кстати, в C # есть метод сравнения и замены в виде библиотечного метода, который можно сопоставить с машинной инструкцией. Я полагаю, что у Java нет такого метода, поскольку он не может быть представлен в системе типов Java.

Эрик Липперт
источник
8
Если бы кто-нибудь мог пройти через весь JLS, тогда не было бы никакой причины публиковать книги по Java, и хотя бы половина этого сайта была бы бесполезной.
Джон Макклейн
8
@JohnMcClane: Уверяю вас, нет никаких трудностей в прохождении всей спецификации, но также вам не нужно. Спецификация Java начинается с полезного «оглавления», которое поможет вам быстро добраться до частей, которые вам наиболее интересны. Он также доступен в Интернете и с возможностью поиска по ключевым словам. Тем не менее, вы правы: есть много хороших ресурсов, которые помогут вам узнать, как работает Java; Мой тебе совет, чтобы ты использовал их!
Эрик Липперт
8
Этот ответ излишне снисходителен и груб. Помни: будь милым .
Вален
7
@ LuisG .: Никакое снисхождение не предназначено или подразумевается; мы все здесь, чтобы учиться друг у друга, и я не рекомендую ничего, что я не делал сам, когда был новичком. И это не грубо. Четко и недвусмысленно идентифицировать их ложные убеждения - это доброта к оригинальному постеру . Скрываться за «вежливостью» и позволять людям продолжать иметь ложные убеждения бесполезно , и усиливает плохие привычки мышления .
Эрик Липперт
5
@LuisG .: Я писал блог о дизайне JavaScript, и самые полезные комментарии, которые я когда-либо получал, были от Брендана, ясно и недвусмысленно указывающего, где я ошибся. Это было здорово, и я ценил его за то, что он нашел время, потому что я прожил следующие 20 лет своей жизни, не повторяя эту ошибку в своей работе или, что еще хуже, обучая ее другим. Это также дало мне возможность исправить те же самые ложные убеждения в других, используя себя в качестве примера того, как люди начинают верить в ложные вещи.
Эрик Липперт
16

Это связано с приоритетом операторов и тем, как оцениваются операторы.

Скобки '()' имеют более высокий приоритет и имеют ассоциативность слева направо. Равенство '==' идет следующим в этом вопросе и имеет ассоциативность слева направо. Задание '=' идет последним и имеет ассоциативность справа налево.

Система использует стек для оценки выражения. Выражение оценивается слева направо.

Теперь приходит к оригинальному вопросу:

int x = 1;
int y = 3;
System.out.println(x == (x = y)); // false

Сначала x (1) будет помещен в стек. тогда внутренняя (x = y) будет оценена и помещена в стек со значением x (3). Теперь x (1) будет сравниваться с x (3), поэтому результат равен false.

x = 1; // reset
System.out.println((x = y) == x); // true

Здесь (x = y) будет оцениваться, теперь значение x станет 3, а x (3) будет помещено в стек. Теперь x (3) с измененным значением после равенства будет помещено в стек. Теперь выражение будет оценено, и оба будут одинаковыми, поэтому результат верен.

Amit
источник
12

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

С участием:

      x == (x = y)

Вы в основном делаете то же самое, что и:

      x == y

И х будет иметь значение у после сравнения.

Хотя с:

      (x = y) == x

Вы в основном делаете то же самое, что и:

      x == x

После й взял у «s значения. И это всегда вернет истину .

Or10n
источник
9

В первом тесте вы проверяете 1 == 3.

Во втором тесте ваша проверка делает 3 == 3.

(x = y) присваивает значение, и это значение проверяется. В первом примере сначала x = 1, затем присваивается x 3. 1 == 3?

В последнем случае х назначается 3, и, очевидно, это все еще 3. 3 == 3?

Майкл Пакетт II
источник
8

Рассмотрим другой, возможно, более простой пример:

int x = 1;
System.out.println(x == ++x); // false
x = 1; // reset
System.out.println(++x == x); // true

Здесь оператор предварительного увеличения ++xдолжен быть применен до того, как будет выполнено сравнение - так же, как (x = y)в вашем примере, должен быть вычислен до сравнения.

Тем не менее, оценка выражения по-прежнему происходит слева направо , так что первое сравнение на самом деле, 1 == 2а второе 2 == 2.
То же самое происходит в вашем примере.

Walen
источник
8

Выражения оцениваются слева направо. В таком случае:

int x = 1;
int y = 3;

x == (x = y)) // false
x ==    t

- left x = 1
- let t = (x = y) => x = 3
- x == (x = y)
  x == t
  1 == 3 //false

(x = y) == x); // true
   t    == x

- left (x = y) => x = 3
           t    =      3 
-  (x = y) == x
-     t    == x
-     3    == 3 //true
Дервиш Каймбашыоглу
источник
5

По сути, первое утверждение x имеет значение 1, поэтому Java сравнивает 1 == с новой переменной x, которая не будет такой же

Во втором вы сказали x = y, что означает, что значение x изменилось, и поэтому, когда вы вызовете его снова, оно будет таким же, следовательно, почему оно истинно, а x == x

Ахмад Бедирксан
источник
4

== является оператором равенства сравнения и работает слева направо.

x == (x = y);

здесь старое назначенное значение x сравнивается с новым назначенным значением x, (1 == 3) // false

(x = y) == x;

Принимая во внимание, что здесь новое присвоенное значение x сравнивается с новым удерживающим значением x, присвоенным ему непосредственно перед сравнением, (3 == 3) // true

Теперь рассмотрим это

    System.out.println((8 + (5 * 6)) * 9);
    System.out.println(8 + (5 * 6) * 9);
    System.out.println((8 + 5) * 6 * 9);
    System.out.println((8 + (5) * 6) * 9);
    System.out.println(8 + 5 * 6 * 9);

Вывод:

342

278

702

342

278

Таким образом, круглые скобки играют основную роль в арифметических выражениях, а не в выражениях сравнения.

Нисрин Дундиа
источник
1
Вывод неверный. Поведение не отличается между арифметическими и операторами сравнения. x + (x = y)и (x = y) + xпоказал бы поведение, подобное оригиналу с операторами сравнения.
JJJ
1
@JJJ В x + (x = y) и (x = y) + x сравнение не проводится, это просто присвоение значения y для x и добавление его к x.
Nisrin Dhoondia
1
... да, в этом все дело. «Круглые скобки играют основную роль в арифметических выражениях, а не в выражениях сравнения» - это неправильно, потому что нет разницы между арифметическими и сравнительными выражениями.
JJJ
2

Дело в том, что порядок приоритетов арифметических операторов / реляционных операторов из двух операторов по =сравнению ==с доминирующим ==(Реляционные операторы доминирует), так как он предшествует =операторам присваивания. Несмотря на приоритет, порядок оценки - LTR (СЛЕВА ПРАВО), приоритет проявляется после порядка оценки. Итак, независимо от каких-либо ограничений оценка является LTR.

Химаншу Ахуджа
источник
1
Ответ неверный. Приоритет оператора не влияет на порядок оценки. Прочитайте некоторые из наиболее популярных ответов для объяснения, особенно этот .
JJJ
1
Правильно, это то, как нас учили, что иллюзия ограничений приоритета приходит ко всем этим вещам, но правильно указывает, что это никак не влияет, потому что порядок оценки остается слева направо
Химаншу Ахаджа
-1

Легко во втором сравнении слева это присвоение после присвоения y x (слева), затем вы сравниваете 3 == 3. В первом примере вы сравниваете x = 1 с новым назначением x = 3. Кажется, что всегда принимаются текущие состояния чтения операторов слева направо от х.

Михал Зиобро
источник
-2

Вопрос, который вы задали, является очень хорошим вопросом, если вы хотите написать компилятор Java или протестировать программы, чтобы убедиться, что компилятор Java работает правильно. В Java эти два выражения должны давать результаты, которые вы видели. Например, в C ++ им это не нужно - поэтому, если кто-то повторно использует части компилятора C ++ в их компиляторе Java, вы можете теоретически обнаружить, что компилятор ведет себя не так, как должен.

Как разработчик программного обеспечения, пишущий код, который читабелен, понятен и удобен в обслуживании, обе версии вашего кода будут считаться ужасными. Чтобы понять, что делает код, нужно точно знать , как определяется язык Java. Кто-то, кто пишет код на Java и C ++, вздрогнул бы, глядя на код. Если вам нужно спросить, почему одна строка кода делает то, что делает, то вам следует избегать этого кода. (Полагаю и надеюсь, что ребята, которые правильно ответили на ваш вопрос «почему», сами тоже избежат этого кода).

gnasher729
источник
«Чтобы понять, что делает код, нужно точно знать, как определяется язык Java». Но что, если каждый сотрудник считает это здравым смыслом?
BinaryTreeee