Por que x == (x = y) não é o mesmo que (x = y) == x?

207

Considere o seguinte exemplo:

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
     }
}

Não tenho certeza se existe um item na Especificação de Linguagem Java que determina o carregamento do valor anterior de uma variável para comparação com o lado direito ( x = y) que, pela ordem implícita entre colchetes, deve ser calculado primeiro.

Por que a primeira expressão é avaliada como false, mas a segunda é avaliada como true? Eu esperava (x = y)ser avaliado primeiro e, depois, comparar x-se ( 3) e retornar true.


Essa questão é diferente da ordem de avaliação das subexpressões em uma expressão Java , que xdefinitivamente não é uma 'subexpressão' aqui. Ele precisa ser carregado para a comparação, em vez de ser 'avaliado'. A pergunta é específica de Java e a expressão x == (x = y), ao contrário de construções práticas pouco comuns, geralmente criadas para perguntas complicadas de entrevistas, veio de um projeto real. Era para ser uma substituição de uma linha para o idioma comparar e substituir

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

que, sendo ainda mais simples que a instrução x86 CMPXCHG, merecia uma expressão mais curta em Java.

John McClane
fonte
62
O lado esquerdo é sempre avaliado antes do lado direito. Os colchetes não fazem diferença nisso.
Louis Wasserman
11
Avaliar a expressão x = yé certamente relevante e causa o efeito colateral xdefinido como o valor de y.
Louis Wasserman
50
Faça um favor a si e a seus colegas de equipe e não misture a mutação de estado na mesma linha do exame de estado. Fazer isso reduz drasticamente a legibilidade do seu código. (Existem alguns casos em que seja absolutamente necessário por causa das exigências de atomicidade, mas funciona para aqueles que já existem e sua finalidade seria imediatamente reconhecida.)
jpmc26
50
A verdadeira questão é por que você deseja escrever um código como este.
klutt
26
A chave para sua pergunta é sua crença falsa de que parênteses implicam ordem de avaliação. Essa é uma crença comum por causa de como aprendemos matemática no ensino fundamental e porque alguns livros de programação para iniciantes ainda entendem errado , mas é uma crença falsa. Esta é uma pergunta bastante frequente. Você pode se beneficiar lendo meus artigos sobre o assunto; eles são sobre C #, mas se aplicam ao Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Eric Lippert

Respostas:

97

que, pela ordem implícita entre parênteses, deve ser calculada primeiro

Não. É um equívoco comum que os parênteses tenham algum efeito (geral) na ordem de cálculo ou avaliação. Eles apenas coagem as partes da sua expressão em uma árvore específica, vinculando os operandos certos às operações certas para o trabalho.

(E, se você não usá-los, essas informações são provenientes da "precedência" e associatividade dos operadores, algo que é resultado de como a árvore de sintaxe da linguagem é definida. Na verdade, ainda é exatamente assim que funciona quando você use parênteses, mas simplificamos e dizemos que não confiamos em nenhuma regra de precedência.)

Uma vez feito isso (ou seja, uma vez que seu código foi analisado em um programa), esses operandos ainda precisam ser avaliados e existem regras separadas sobre como isso é feito: as regras ditas (como Andrew nos mostrou) afirmam que o LHS de cada operação é avaliado primeiro em Java.

Observe que esse não é o caso em todos os idiomas; por exemplo, em C ++, a menos que você esteja usando um operador de curto-circuito como &&ou ||, a ordem de avaliação dos operandos geralmente não é especificada e você não deve confiar nela de nenhuma maneira.

Os professores precisam parar de explicar a precedência do operador usando frases enganosas como "isso faz a adição acontecer primeiro". Dada uma expressão, x * y + za explicação correta seria "a precedência do operador faz a adição acontecer entre x * ye z, em vez de entre ye z", sem mencionar nenhuma "ordem".

Raças de leveza em órbita
fonte
6
Eu gostaria que meus professores tivessem feito alguma separação entre a matemática subjacente e a sintaxe que eles usavam para representá-la, como se passássemos um dia com números romanos ou notação polonesa ou qualquer outra coisa e vimos que essa adição tem as mesmas propriedades. Aprendemos associatividade e todas essas propriedades no ensino médio, portanto, havia bastante tempo.
John P
1
Que bom que você mencionou que esta regra não se aplica a todos os idiomas. Além disso, se um dos lados tiver outro efeito colateral, como gravar em um arquivo ou ler a hora atual, ele (mesmo em Java) será indefinido em que ordem isso acontece. No entanto, o resultado da comparação será como se tivesse sido avaliado da esquerda para a direita (em Java). Outro aspecto: algumas linguagens simplesmente não permitem a atribuição de mixagens e comparações dessa maneira por regras de sintaxe, e o problema não surgiria.
Abel
5
@ JohnP: Fica pior. 5 * 4 significa 5 + 5 + 5 + 5 ou 4 + 4 + 4 + 4 + 4? Alguns professores insistem que apenas uma dessas opções é correta.
18718 Brian
3
@ Brian Mas ... mas ... a multiplicação de números reais é comutativa!
Lightness Races em órbita
2
No meu mundo de pensamento, um par de parênteses representa "é necessário". Calculando 'a * (b + c)', os parênteses expressariam que o resultado da adição é necessário para a multiplicação. Quaisquer preferências implícitas do operador podem ser expressas por parênteses, exceto regras LHS primeiro ou RHS primeiro. (Isso é verdade?) @Brian Em matemática, existem alguns casos raros em que a multiplicação pode ser substituída por adição repetida, mas isso nem sempre é verdade (começando com números complexos, mas não se limitando a). Assim, seus educadores devem realmente ter um olho sobre o que as pessoas estão dizendo ....
Syck
164

==é um operador de igualdade binária .

O operando do lado esquerdo de um operador binário parece ser totalmente avaliado antes de qualquer parte do operando do lado direito ser avaliada.

Especificação do Java 11> Ordem de avaliação> Avaliar primeiro o operando do lado esquerdo

Andrew Tobilko
fonte
42
A expressão "parece ser" não parece ter certeza, tbh.
Mr Lister
86
"parece ser" significa que a especificação não exige que as operações sejam realmente executadas nessa ordem cronologicamente, mas exige que você obtenha o mesmo resultado que obteria se fossem.
Robyn
24
@MrLister "parece ser" parece ser uma má escolha de palavras da parte deles. Por "aparecer", eles significam "manifestar como um fenômeno para o desenvolvedor". "é eficaz" pode ser uma frase melhor.
Kelvin13 /
18
na comunidade C ++, isso é equivalente à regra "como se" ... é necessário que o operando se comporte "como se" fosse implementado pelas seguintes regras, mesmo que tecnicamente não seja.
Michael Edenfield
2
@ Kelvin eu concordo, eu teria escolhido essa palavra também, ao invés de "parece ser".
MC Emperor
149

Como LouisWasserman disse, a expressão é avaliada da esquerda para a direita. E o java não se importa com o que "avaliar" realmente faz, apenas se preocupa em gerar um valor (não volátil, final) para trabalhar.

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

Portanto, para calcular a primeira saída de System.out.println(), é feito o seguinte:

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

e para calcular o segundo:

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

Observe que o segundo valor sempre será avaliado como verdadeiro, independentemente dos valores iniciais de xe y, porque você está comparando efetivamente a atribuição de um valor à variável à qual ele foi atribuído a = be bserá avaliado nessa ordem e sempre será o mesmo por definição.

Poohl
fonte
"Esquerda para a direita" também é verdade em matemática, a propósito, exatamente quando você chega a parênteses ou precedência, itera dentro deles e avalia tudo dentro da esquerda para a direita antes de prosseguir na camada principal. Mas a matemática nunca faria isso; a distinção importa apenas porque esta não é uma equação, mas uma operação combinada, executando uma atribuição e uma equação de uma só vez . Eu nunca faria isso porque a legibilidade é ruim, a menos que estivesse praticando golfe com código ou procurando uma maneira de otimizar o desempenho e, então, haveria comentários.
Harper - Restabelece Monica
25

Não tenho certeza se existe um item na Especificação de Linguagem Java que determina o carregamento do valor anterior de uma variável ...

Há sim. Da próxima vez que você está claro o que a especificação diz, por favor leia a especificação e , em seguida, fazer a pergunta se não está claro.

... o lado direito (x = y)que, pela ordem implícita entre parênteses, deve ser calculado primeiro.

Essa afirmação é falsa. Parênteses não implicam uma ordem de avaliação . Em Java, a ordem da avaliação é da esquerda para a direita, independentemente dos parênteses. Parênteses determinam onde estão os limites da subexpressão, e não a ordem da avaliação.

Por que a primeira expressão é avaliada como falsa, mas a segunda é avaliada como verdadeira?

A regra para o ==operador é: avalie o lado esquerdo para produzir um valor, avalie o lado direito para produzir um valor, compare os valores, a comparação é o valor da expressão.

Em outras palavras, o significado de expr1 == expr2é sempre o mesmo que se você tivesse escrito temp1 = expr1; temp2 = expr2;e avaliado temp1 == temp2.

A regra para o =operador com uma variável local no lado esquerdo é: avalie o lado esquerdo para produzir uma variável, avalie o lado direito para produzir um valor, execute a atribuição, o resultado é o valor que foi atribuído.

Então junte-o:

x == (x = y)

Temos um operador de comparação. Avalie o lado esquerdo para produzir um valor - obtemos o valor atual de x. Avalie o lado direito: é uma atribuição, portanto, avaliamos o lado esquerdo para produzir uma variável - a variável x- avaliamos o lado direito - o valor atual de y- atribua a ela xe o resultado é o valor atribuído. Em seguida, comparamos o valor original xcom o valor atribuído.

Você pode fazer (x = y) == xcomo um exercício. Novamente, lembre-se de que todas as regras para avaliar o lado esquerdo acontecem antes de todas as regras de avaliação do lado direito .

Eu esperaria que (x = y) fosse avaliado primeiro e, em seguida, ele compararia x consigo (3) e retornaria true.

Sua expectativa é baseada em um conjunto de crenças incorretas sobre as regras do Java. Espero que agora você tenha crenças corretas e, no futuro, espere coisas verdadeiras.

Esta pergunta é diferente de "ordem de avaliação de subexpressões em uma expressão Java"

Esta afirmação é falsa. Essa pergunta é totalmente pertinente.

x definitivamente não é uma 'subexpressão' aqui.

Esta afirmação também é falsa. É uma subexpressão duas vezes em cada exemplo.

Ele precisa ser carregado para a comparação, em vez de ser 'avaliado'.

Eu não tenho ideia do que isso significa.

Aparentemente, você ainda tem muitas crenças falsas. Meu conselho é que você leia a especificação até que suas falsas crenças sejam substituídas por verdadeiras.

A pergunta é específica de Java e a expressão x == (x = y), ao contrário de construções práticas pouco buscadas comumente criadas para perguntas difíceis de entrevistas, veio de um projeto real.

A proveniência da expressão não é relevante para a questão. As regras para tais expressões estão claramente descritas na especificação; Leia-o!

Era para ser uma substituição de uma linha para o idioma comparar e substituir

Como essa substituição de uma linha causou muita confusão em você, leitor do código, eu sugeriria que era uma má escolha. Tornar o código mais conciso, mas mais difícil de entender, não é uma vitória. É improvável que o código seja mais rápido.

Aliás, o C # compara e substitui como um método de biblioteca, que pode ser associado a uma instrução de máquina. Acredito que o Java não possui esse método, pois não pode ser representado no sistema do tipo Java.

Eric Lippert
fonte
8
Se alguém pudesse passar por todo o JLS, não haveria razão para publicar livros em Java e pelo menos metade desse site também seria inútil.
John McClane
8
@JohnMcClane: Garanto-lhe que não há nenhuma dificuldade em passar por toda a especificação, mas também não é necessário. A especificação Java começa com um útil "índice", que o ajudará a chegar rapidamente às partes nas quais você está mais interessado. Também é on-line e pesquisável por palavras-chave. Dito isto, você está correto: existem muitos bons recursos que ajudarão você a aprender como o Java funciona; meu conselho para você é que você faça uso deles!
Eric Lippert
8
Essa resposta é desnecessariamente condescendente e rude. Lembre-se: seja legal .
walen
7
@LuisG .: Nenhuma condescendência é intencional ou implícita; estamos todos aqui para aprender um com o outro, e não recomendo nada que eu não tenha feito quando era iniciante. Nem é rude. Identificar clara e inequivocamente suas falsas crenças é uma bondade para o pôster original . Esconder-se atrás da "polidez" e permitir que as pessoas continuem a ter falsas crenças é inútil e reforça os maus hábitos de pensamento .
precisa
5
@LuisG .: Eu costumava escrever um blog sobre o design do JavaScript, e os comentários mais úteis que recebi foram de Brendan, apontando clara e inequivocamente onde eu errei. Isso foi ótimo e eu apreciei que ele tomasse algum tempo, porque então vivi os próximos 20 anos da minha vida sem repetir esse erro no meu próprio trabalho, ou pior, ensinando-o a outras pessoas. Também me deu a oportunidade de corrigir essas mesmas falsas crenças nos outros, usando-me como um exemplo de como as pessoas passam a acreditar em coisas falsas.
precisa
16

Está relacionado à precedência do operador e como os operadores estão sendo avaliados.

Os parênteses '()' têm maior precedência e associatividade da esquerda para a direita. A igualdade '==' vem em seguida nesta pergunta e tem associatividade da esquerda para a direita. A tarefa '=' vem por último e tem associatividade da direita para a esquerda.

O sistema usa a pilha para avaliar a expressão. A expressão é avaliada da esquerda para a direita.

Agora chega à pergunta original:

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

Primeiro x (1) será pressionado para empilhar. então interno (x = y) será avaliado e pressionado para empilhar com o valor x (3). Agora x (1) será comparado com x (3), portanto o resultado é falso.

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

Aqui, (x = y) será avaliado, agora o valor de x se tornará 3 e x (3) será pressionado para empilhar. Agora x (3) com valor alterado após igualdade será empurrado para empilhar. Agora a expressão será avaliada e ambas serão iguais, portanto o resultado é verdadeiro.

Amit
fonte
12

Não é o mesmo. O lado esquerdo sempre será avaliado antes do lado direito, e os colchetes não especificam uma ordem de execução, mas um agrupamento de comandos.

Com:

      x == (x = y)

Você está basicamente fazendo o mesmo que:

      x == y

E x terá o valor de y após a comparação.

Enquanto estiver com:

      (x = y) == x

Você está basicamente fazendo o mesmo que:

      x == x

Depois de x assumiu o valor de y . E sempre retornará verdadeiro .

Or10n
fonte
9

No primeiro teste que você está verificando, faz 1 == 3.

No segundo teste, sua verificação faz 3 == 3.

(x = y) atribui o valor e esse valor é testado. No exemplo anterior, x = 1 primeiro, depois x é atribuído 3. 1 == 3?

No último, x é atribuído 3 e, obviamente, ainda é 3. Será que 3 == 3?

Michael Puckett II
fonte
8

Considere este outro exemplo, talvez mais simples:

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

Aqui, o operador de pré-incremento em ++xdeve ser aplicado antes da comparação, assim como (x = y)no seu exemplo, deve ser calculado antes da comparação.

No entanto, a avaliação da expressão ainda acontece da esquerda para a direita , portanto, a primeira comparação é na verdade 1 == 2a segunda 2 == 2.
O mesmo acontece no seu exemplo.

Walen
fonte
8

As expressões são avaliadas da esquerda para a direita. Nesse caso:

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
Derviş Kayımbaşıoğlu
fonte
5

Basicamente, a primeira instrução x tinha seu valor 1. Então, Java compara 1 == com a nova variável x, que não será a mesma

No segundo, você disse que x = y, o que significa que o valor de x mudou e, portanto, quando você o chamar novamente, será o mesmo valor, portanto, por que é verdadeiro e x == x

Ahmad Bedirxan
fonte
4

== é um operador de igualdade de comparação e funciona da esquerda para a direita.

x == (x = y);

aqui o antigo valor atribuído de x é comparado com o novo valor atribuído de x, (1 == 3) // false

(x = y) == x;

Considerando que aqui o novo valor atribuído de x é comparado com o novo valor de retenção de x atribuído a ele imediatamente antes da comparação, (3 == 3) // true

Agora considere isso

    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);

Resultado:

342

278

702

342

278

Assim, parênteses desempenha seu papel principal em expressões aritméticas, e não apenas em expressões de comparação.

Nisrin Dhoondia
fonte
1
A conclusão está errada. O comportamento não é diferente entre operadores aritméticos e de comparação. x + (x = y)e (x = y) + xmostraria um comportamento semelhante ao original com operadores de comparação.
22418 JJJ
1
@JJJ Em x + (x = y) e (x = y) + x não há comparação envolvida, é apenas atribuir o valor de y a x e adicioná-lo a x.
Nisrin Dhoondia
1
... Sim, esse é o ponto. "Parênteses desempenha seu papel principal em expressões aritméticas, mas não em expressões de comparação" está errado porque não há diferença entre expressões aritméticas e de comparação.
JJJ
2

A questão aqui é a ordem de precedência dos operadores aritmáticos / operadores relacionais dos dois operadores =versus ==o dominante é ==(Operators Relational domina), pois precede os =operadores de atribuição. Apesar da precedência, a ordem de avaliação é LTR (esquerda para a direita). A ordem de avaliação é mostrada após ordem de avaliação. Portanto, independentemente de qualquer avaliação de restrições, é LTR.

Himanshu Ahuja
fonte
1
A resposta está errada. A precedência do operador não afeta a ordem de avaliação. Leia algumas das respostas mais votadas para obter explicações, especialmente esta .
JJJ
1
Correto, a sua realmente a forma como são ensinados a ilusão de restrições à precedência vem n todas essas coisas, mas apontou corretamente não tem impacto coz a ordem de restos de avaliação esquerda para a direita
Himanshu Ahuja
-1

É fácil, na segunda comparação à esquerda, a atribuição após atribuir y a x (à esquerda) e comparar 3 == 3. No primeiro exemplo, você está comparando x = 1 com a nova atribuição x = 3. Parece que sempre são feitas declarações de leitura de estado atual da esquerda para a direita de x.

Michał Ziobro
fonte
-2

O tipo de pergunta que você fez é uma pergunta muito boa se você deseja escrever um compilador Java ou programas de teste para verificar se um compilador Java está funcionando corretamente. Em Java, essas duas expressões devem produzir os resultados que você viu. No C ++, por exemplo, eles não precisam - portanto, se alguém reutilizou partes de um compilador C ++ em seu compilador Java, teoricamente você pode achar que o compilador não se comporta como deveria.

Como desenvolvedor de software, escrevendo um código legível, compreensível e sustentável, ambas as versões do seu código seriam consideradas terríveis. Para entender o que o código faz, é preciso saber exatamente como a linguagem Java é definida. Alguém que escreve código Java e C ++ estremeceria ao olhar para o código. Se você precisar perguntar por que uma única linha de código faz o que faz, evite esse código. (Suponho e espero que os caras que responderam corretamente à sua pergunta "por que" também evitem esse código).

gnasher729
fonte
"Para entender o que o código faz, é preciso saber exatamente como a linguagem Java é definida." Mas e se todos os colegas de trabalho considerarem isso um senso comum?
BinaryTreeee