¿Por qué x == (x = y) no es lo mismo que (x = y) == x?

207

Considere el siguiente ejemplo:

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

No estoy seguro de si hay un elemento en la Especificación del lenguaje Java que dicta cargar el valor anterior de una variable para comparar con el lado derecho (x = y ) que, por el orden implicado entre paréntesis, debe calcularse primero.

¿Por qué la primera expresión evalúa a false, pero la segunda evalúa a true? Hubiera esperado (x = y)ser evaluado primero, y luego se compararía xconsigo mismo ( 3) y volvería true.


Esta pregunta es diferente del orden de evaluación de subexpresiones en una expresión Java, ya que xdefinitivamente no es una 'subexpresión' aquí. Debe cargarse para la comparación en lugar de ser 'evaluado'. La pregunta es específica de Java y la expresión x == (x = y), a diferencia de los constructos poco prácticos y extravagantes comúnmente diseñados para preguntas difíciles de la entrevista, provino de un proyecto real. Se suponía que era un reemplazo de una línea para el idioma de comparar y reemplazar

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

que, siendo aún más simple que la instrucción x86 CMPXCHG, merecía una expresión más corta en Java.

John McClane
fuente
62
El lado izquierdo siempre se evalúa antes que el lado derecho. Los corchetes no hacen una diferencia en eso.
Louis Wasserman
11
Evaluar la expresión x = yes ciertamente relevante y causa el efecto secundario que xse establece en el valor de y.
Louis Wasserman
50
Hazte un favor a ti y a tus compañeros de equipo y no mezcles la mutación de estado en la misma línea que el examen de estado. Hacerlo reduce drásticamente la legibilidad de su código. (Hay algunos casos en los que es absolutamente necesario debido a los requisitos de atomicidad, pero las funciones para las que ya existen y su propósito serían reconocidos al instante.)
jpmc26
50
La verdadera pregunta es por qué quieres escribir código como este.
klutt
26
La clave de su pregunta es su falsa creencia de que los paréntesis implican un orden de evaluación. Esa es una creencia común debido a cómo nos enseñan matemáticas en la escuela primaria y porque algunos libros de programación para principiantes todavía se equivocan , pero es una creencia falsa. Esta es una pregunta bastante frecuente. Podría beneficiarse al leer mis artículos sobre el tema; son sobre C # pero se aplican a Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Eric Lippert

Respuestas:

97

que, por el orden implicado entre paréntesis, debe calcularse primero

No. Es un error común pensar que los paréntesis tienen algún efecto (general) en el orden de cálculo o evaluación. Solo fuerzan las partes de su expresión en un árbol en particular, vinculando los operandos correctos a las operaciones correctas para el trabajo.

(Y, si no los usa, esta información proviene de la "precedencia" y la asociatividad de los operadores, algo que es el resultado de cómo se define el árbol de sintaxis del lenguaje. De hecho, así es exactamente cómo funciona cuando usted usamos paréntesis, pero simplificamos y decimos que no confiamos en ninguna regla de precedencia entonces).

Una vez hecho esto (es decir, una vez que su código se ha analizado en un programa), esos operandos aún deben evaluarse, y hay reglas separadas sobre cómo se hace: dichas reglas (como Andrew nos ha mostrado) establecen que el LHS de cada operación se evalúa primero en Java.

Tenga en cuenta que este no es el caso en todos los idiomas; por ejemplo, en C ++, a menos que esté utilizando un operador de cortocircuito como &&o ||, el orden de evaluación de los operandos generalmente no está especificado y no debe confiar en él de ninguna manera.

Los maestros deben dejar de explicar la precedencia del operador utilizando frases engañosas como "esto hace que la adición suceda primero". Dada una expresión, x * y + zla explicación adecuada sería "la precedencia del operador hace que la adición ocurra entre x * yy z, en lugar de entre yy z", sin mencionar ningún "orden".

Carreras de ligereza en órbita
fuente
66
Desearía que mis maestros hubieran hecho alguna separación entre las matemáticas subyacentes y la sintaxis que solían representar, como si pasáramos un día con números romanos o con notación polaca o lo que sea y viéramos que la suma tiene las mismas propiedades. Aprendimos asociatividad y todas esas propiedades en la escuela secundaria, así que había mucho tiempo.
John P
1
Me alegra que hayas mencionado que esta regla no es válida para todos los idiomas. Además, si cualquiera de los lados tiene otro efecto secundario, como escribir en un archivo o leer la hora actual, (incluso en Java) no está definido en qué orden sucede. Sin embargo, el resultado de la comparación será como si se evaluara de izquierda a derecha (en Java). Otro lado: bastantes idiomas simplemente no permiten mezclar la asignación y la comparación de esta manera por las reglas de sintaxis, y el problema no surgiría.
Abel
55
@JohnP: Se pone peor. ¿5 * 4 significa 5 + 5 + 5 + 5 o 4 + 4 + 4 + 4 + 4? Algunos maestros insisten en que solo una de esas opciones es correcta.
Brian
3
@Brian Pero ... pero ... ¡la multiplicación de números reales es conmutativa!
Carreras de ligereza en órbita el
2
En mi mundo de pensamiento, un par de paréntesis representa "es necesario para". Calculando ´a * (b + c) ´, los paréntesis expresarían que el resultado de la suma es necesario para la multiplicación. Cualquier preferencia de operador implícita puede expresarse por parens, excepto las reglas LHS-first o RHS-first. (¿Es eso cierto?) @Brian En matemáticas hay algunos casos raros en los que la multiplicación puede ser sustituida por la suma repetida, pero eso no siempre es cierto (comenzando con números complejos pero no limitados a). Por lo que sus educadores deben realmente tener un ojo en lo que las personas están diciendo ....
Syck
164

==es un operador binario de igualdad .

El operando de la izquierda de un operador binario parece evaluarse completamente antes de evaluar cualquier parte del operando de la derecha .

Especificación de Java 11> Orden de evaluación> Evaluar primero el operando de la mano izquierda

Andrew Tobilko
fuente
42
La redacción "parece ser" no parece segura, tbh.
Sr. Lister
86
"parece ser" significa que la especificación no requiere que las operaciones se realicen realmente en ese orden cronológicamente, pero requiere que obtenga el mismo resultado que obtendría si lo fuera.
Robyn
24
@MrLister "parece ser" parece ser una mala elección de palabras por su parte. Por "aparecer" se refieren a "manifestar como un fenómeno para el desarrollador". "es efectivamente" puede ser una mejor frase.
Kelvin
18
en la comunidad C ++, esto es equivalente a la regla "como si" ... el operando debe comportarse "como si" se implementara según las siguientes reglas, incluso si técnicamente no lo es.
Michael Edenfield
2
@ Kelvin Estoy de acuerdo, habría elegido esa palabra, en lugar de "parece ser".
MC Emperor
149

Como dijo LouisWasserman, la expresión se evalúa de izquierda a derecha. Y a Java no le importa lo que realmente hace "evaluar", solo le importa generar un valor (no volátil, final) para trabajar.

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

Entonces, para calcular la primera salida de System.out.println(), se hace lo siguiente:

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

y para calcular el segundo:

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

Tenga en cuenta que el segundo valor siempre se evaluará como verdadero, independientemente de los valores iniciales de xy y, porque está comparando efectivamente la asignación de un valor con la variable a la que está asignado y, a = by bserá, evaluado en ese orden, siempre será el mismo por definición.

Poohl
fuente
"De izquierda a derecha" también es cierto en matemáticas, por cierto, justo cuando llegas a un paréntesis o precedencia, iteras dentro de ellos y evalúas todo dentro de izquierda a derecha antes de continuar en el nivel principal. Pero las matemáticas nunca harían esto; la distinción solo importa porque no se trata de una ecuación sino de una operación combinada, que realiza tanto una asignación como una ecuación de una sola vez . Nunca haría esto porque la legibilidad es pobre, a menos que estuviera haciendo golf de código o estuviera buscando una forma de optimizar el rendimiento, y luego, habría comentarios.
Harper - Restablece a Mónica el
25

No estoy seguro de si hay un elemento en la Especificación del lenguaje Java que dicta cargar el valor anterior de una variable ...

Ahi esta. La próxima vez que no tenga claro qué dice la especificación, lea la especificación y luego haga la pregunta si no está clara.

... el lado derecho (x = y)que, por el orden implicado entre paréntesis, debe calcularse primero.

Esa afirmación es falsa. Los paréntesis no implican un orden de evaluación . En Java, el orden de evaluación es de izquierda a derecha, independientemente de los paréntesis. Los paréntesis determinan dónde están los límites de subexpresión, no el orden de evaluación.

¿Por qué la primera expresión se evalúa como falsa, pero la segunda se evalúa como verdadera?

La regla para el ==operador es: evaluar el lado izquierdo para producir un valor, evaluar el lado derecho para producir un valor, comparar los valores, la comparación es el valor de la expresión.

En otras palabras, el significado de expr1 == expr2siempre es el mismo que si hubiera escrito temp1 = expr1; temp2 = expr2;y luego evaluado temp1 == temp2.

La regla para el =operador con una variable local en el lado izquierdo es: evaluar el lado izquierdo para producir una variable, evaluar el lado derecho para producir un valor, realizar la asignación, el resultado es el valor asignado.

Así que póngalo junto:

x == (x = y)

Tenemos un operador de comparación. Evalúe el lado izquierdo para producir un valor; obtenemos el valor actual de x. Evalúe el lado derecho: esa es una asignación, por lo que evaluamos el lado izquierdo para producir una variable, la variable x, evaluamos el lado derecho, el valor actual de y, se lo asignamos xy el resultado es el valor asignado. Luego comparamos el valor original de xcon el valor asignado.

Puedes hacer (x = y) == xcomo ejercicio. Nuevamente, recuerde, todas las reglas para evaluar el lado izquierdo suceden antes que todas las reglas para evaluar el lado derecho .

Hubiera esperado que (x = y) fuera evaluado primero, y luego compararía x consigo mismo (3) y devolvería verdadero.

Su expectativa se basa en un conjunto de creencias incorrectas sobre las reglas de Java. Esperemos que ahora tenga las creencias correctas y espere en el futuro cosas verdaderas.

Esta pregunta es diferente del "orden de evaluación de subexpresiones en una expresión Java"

Esta afirmación es falsa. Esa pregunta es totalmente pertinente.

x definitivamente no es una 'subexpresión' aquí.

Esta afirmación también es falsa. Es una subexpresión dos veces en cada ejemplo.

Debe cargarse para la comparación en lugar de ser 'evaluado'.

No tengo idea de lo que esto significa.

Aparentemente todavía tienes muchas creencias falsas. Mi consejo es que leas la especificación hasta que tus creencias falsas sean reemplazadas por creencias verdaderas.

La pregunta es específica de Java y la expresión x == (x = y), a diferencia de los constructos poco prácticos y extravagantes comúnmente diseñados para preguntas difíciles de la entrevista, provino de un proyecto real.

La procedencia de la expresión no es relevante para la pregunta. Las reglas para tales expresiones se describen claramente en la especificación; Léelo!

Se suponía que era un reemplazo de una línea para el lenguaje de comparar y reemplazar

Como ese reemplazo de una línea causó mucha confusión en usted, el lector del código, sugeriría que fue una mala elección. Hacer que el código sea más conciso pero más difícil de entender no es una victoria. Es poco probable que el código sea más rápido.

Por cierto, C # tiene comparar y reemplazar como un método de biblioteca, que puede ser reducido a una instrucción de máquina. Creo que Java no tiene dicho método, ya que no se puede representar en el sistema de tipos Java.

Eric Lippert
fuente
8
Si alguien pudiera pasar por todo el JLS, entonces no habría razón para publicar libros Java y al menos la mitad de este sitio también sería inútil.
John McClane
8
@JohnMcClane: Le aseguro que no hay ninguna dificultad en revisar toda la especificación, pero tampoco tiene que hacerlo. La especificación de Java comienza con una útil "tabla de contenido" que lo ayudará a llegar rápidamente a las partes que más le interesan. También está en línea y puede buscar palabras clave. Dicho esto, tienes razón: hay muchos buenos recursos que te ayudarán a aprender cómo funciona Java; ¡Mi consejo para ti es que los utilices!
Eric Lippert
8
Esta respuesta es innecesariamente condescendiente y grosera. Recuerda: se amable .
walen
77
@LuisG .: Ninguna condescendencia es intencional o implícita; Todos estamos aquí para aprender unos de otros, y no recomiendo nada que no haya hecho yo mismo cuando era principiante. Tampoco es grosero. Identificar clara e inequívocamente sus creencias falsas es una amabilidad con el cartel original . Es inútil esconderse detrás de la "cortesía" y permitir que las personas sigan teniendo creencias falsas , y refuerza los malos hábitos de pensamiento .
Eric Lippert
55
@LuisG .: solía escribir un blog sobre el diseño de JavaScript, y los comentarios más útiles que recibí fueron de Brendan señalando clara e inequívocamente dónde me equivoqué. Eso fue genial y aprecié que se tomara el tiempo, porque luego viví los siguientes 20 años de mi vida sin repetir ese error en mi propio trabajo, o peor aún, enseñándolo a otros. También me dio la oportunidad de corregir esas mismas creencias falsas en otros al usarme a mí mismo como un ejemplo de cómo las personas llegan a creer cosas falsas.
Eric Lippert
16

Está relacionado con la precedencia del operador y cómo se evalúa a los operadores.

Los paréntesis '()' tienen mayor prioridad y asociatividad de izquierda a derecha. La igualdad '==' viene después en esta pregunta y tiene asociatividad de izquierda a derecha. La tarea '=' es la última y tiene asociatividad de derecha a izquierda.

El sistema usa la pila para evaluar la expresión. La expresión se evalúa de izquierda a derecha.

Ahora viene a la pregunta original:

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

Primero x (1) será empujado a la pila. entonces interno (x = y) será evaluado y empujado a la pila con el valor x (3). Ahora x (1) se comparará con x (3), por lo que el resultado es falso.

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

Aquí, (x = y) será evaluado, ahora el valor de x se convierte en 3 yx (3) será empujado a la pila. Ahora x (3) con el valor cambiado después de la igualdad será empujado a la pila. Ahora se evaluará la expresión y ambos serán iguales, por lo que el resultado es verdadero.

Amit
fuente
12

No es lo mismo. El lado izquierdo siempre se evaluará antes que el lado derecho, y los corchetes no especifican un orden de ejecución, sino una agrupación de comandos.

Con:

      x == (x = y)

Básicamente estás haciendo lo mismo que:

      x == y

Y x tendrá el valor de y después de la comparación.

Mientras que con:

      (x = y) == x

Básicamente estás haciendo lo mismo que:

      x == x

Después x tomaron y 's valor. Y siempre volverá cierto .

Or10n
fuente
9

En la primera prueba que estás verificando hace 1 == 3.

En la segunda prueba, su comprobación hace 3 == 3.

(x = y) asigna el valor y ese valor se prueba. En el primer ejemplo, x = 1 primero y luego se asigna x 3. ¿1 == 3?

En este último, a x se le asigna 3, y obviamente sigue siendo 3. ¿3 == 3?

Michael Puckett II
fuente
8

Considere este otro ejemplo, quizás más simple:

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

Aquí, el operador de preincremento ++xdebe aplicarse antes de realizar la comparación, al igual que (x = y)en su ejemplo, debe calcularse antes de la comparación.

Sin embargo, la evaluación de la expresión todavía ocurre de izquierda a derecha , por lo que la primera comparación es en realidad 1 == 2la segunda 2 == 2.
Lo mismo sucede en tu ejemplo.

walen
fuente
8

Las expresiones se evalúan de izquierda a derecha. En este 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
fuente
5

Básicamente, la primera declaración x tenía su valor 1, por lo que Java compara 1 == con la nueva variable x que no será la misma

En el segundo dijiste x = y, lo que significa que el valor de x cambió y, por lo tanto, cuando lo vuelvas a llamar, será el mismo valor, por lo tanto, ¿por qué es verdad y x == x?

Ahmad Bedirxan
fuente
4

== es un operador de igualdad de comparación y funciona de izquierda a derecha.

x == (x = y);

aquí el antiguo valor asignado de x se compara con el nuevo valor asignado de x, (1 == 3) // falso

(x = y) == x;

Mientras que aquí el nuevo valor de asignación de x se compara con el nuevo valor de retención de x asignado justo antes de la comparación, (3 == 3) // verdadero

Ahora considera esto

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

Salida:

342

278

702

342

278

Por lo tanto, los paréntesis juegan su papel principal en las expresiones aritméticas, no solo en las expresiones de comparación.

Nisrin Dhoondia
fuente
1
La conclusión está mal. El comportamiento no es diferente entre los operadores aritméticos y de comparación. x + (x = y)y (x = y) + xmostraría un comportamiento similar al original con operadores de comparación.
JJJ
1
@JJJ En x + (x = y) y (x = y) + x no hay comparación involucrada, solo asigna el valor y a x y lo agrega a x.
Nisrin Dhoondia
1
... sí, ese es el punto. "Los paréntesis juegan su papel principal en las expresiones aritméticas, pero no en las expresiones de comparación" está mal porque no hay diferencia entre las expresiones aritméticas y de comparación.
JJJ
2

La cuestión aquí es el orden de precedencia de los operadores aritmáticos / operadores relacionales de los dos operadores =frente ==al dominante es ==(los operadores relacionales dominan) ya que precede a los =operadores de asignación. A pesar de la precedencia, el orden de evaluación es LTR (IZQUIERDA A DERECHA). La precedencia aparece en la imagen después del orden de evaluación. Entonces, independientemente de cualquier evaluación de restricciones, es LTR.

Himanshu Ahuja
fuente
1
La respuesta es incorrecta. La precedencia del operador no afecta el orden de evaluación. Lea algunas de las respuestas más votadas para obtener una explicación, especialmente esta .
JJJ
1
Correcto, en realidad es la forma en que se nos enseña que la ilusión de las restricciones sobre la precedencia viene en todas esas cosas, pero correctamente señaló que no tiene impacto porque el orden de evaluación permanece de izquierda a derecha
Himanshu Ahuja
-1

Es fácil en la segunda comparación a la izquierda es la asignación después de asignar y a x (a la izquierda) y luego se compara 3 == 3. En el primer ejemplo, se compara x = 1 con la nueva asignación x = 3. Parece que siempre se toman declaraciones de lectura de estado actual de izquierda a derecha de x.

Michał Ziobro
fuente
-2

El tipo de pregunta que hizo es una muy buena pregunta si desea escribir un compilador de Java o probar programas para verificar que un compilador de Java funciona correctamente. En Java, estas dos expresiones deben producir los resultados que vio. En C ++, por ejemplo, no tienen que hacerlo, por lo que si alguien reutiliza partes de un compilador de C ++ en su compilador de Java, teóricamente podría encontrar que el compilador no se comporta como debería.

Como desarrollador de software, escribir código que sea legible, comprensible y mantenible, ambas versiones de su código se considerarían horribles. Para entender lo que hace el código, uno tiene que saber exactamente cómo se define el lenguaje Java. Alguien que escribe código Java y C ++ se estremecería al mirar el código. Si tiene que preguntar por qué una sola línea de código hace lo que hace, entonces debe evitar ese código. (Supongo y espero que los chicos que respondieron correctamente a su pregunta de "por qué" también eviten ese indicio de código).

gnasher729
fuente
"Para entender lo que hace el código, uno tiene que saber exactamente cómo se define el lenguaje Java". Pero, ¿qué pasa si cada compañero de trabajo lo considera un sentido común?
BinaryTreeee