¿Cuáles son las reglas para el orden de evaluación en Java?

86

Estoy leyendo un texto de Java y obtuve el siguiente código:

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

En el texto, el autor no dio una explicación clara y el efecto de la última línea es: a[1] = 0;

No estoy tan seguro de haber entendido: ¿cómo sucedió la evaluación?

ipkiss
fuente
19
La confusión a continuación sobre por qué sucede esto significa, por cierto, que nunca debes hacer esto, ya que muchas personas tendrán que pensar en lo que realmente hace, en lugar de parecer obvio.
Martijn
La respuesta adecuada a cualquier pregunta como esta es "¡no hagas eso!" La cesión debe tratarse como una declaración; el uso de la asignación como una expresión que devuelve un valor debería generar un error del compilador, o al menos una advertencia. No hagas eso.
Mason Wheeler

Respuestas:

173

Permítanme decir esto muy claramente, porque la gente lo entiende mal todo el tiempo:

El orden de evaluación de las subexpresiones es independiente tanto de la asociatividad como de la precedencia . La asociatividad y la precedencia determinan en qué orden se ejecutan los operadores , pero no determinan en qué orden se evalúan las subexpresiones . Su pregunta es sobre el orden en que se evalúan las subexpresiones .

Considere A() + B() + C() * D(). La multiplicación tiene mayor precedencia que la suma, y ​​la suma es asociativa por la izquierda, por lo que esto es equivalente a (A() + B()) + (C() * D()) Pero saber eso solo te dice que la primera suma ocurrirá antes de la segunda suma, y ​​que la multiplicación ocurrirá antes de la segunda suma. ¡No le dice en qué orden se llamarán A (), B (), C () y D ()! (Tampoco le dice si la multiplicación ocurre antes o después de la primera adición). Sería perfectamente posible obedecer las reglas de precedencia y asociatividad compilando esto como:

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

Allí se siguen todas las reglas de precedencia y asociatividad: la primera adición ocurre antes de la segunda adición y la multiplicación ocurre antes de la segunda adición. ¡Claramente podemos hacer las llamadas a A (), B (), C () y D () en cualquier orden y aún obedecer las reglas de precedencia y asociatividad!

Necesitamos una regla no relacionada con las reglas de precedencia y asociatividad para explicar el orden en que se evalúan las subexpresiones. La regla relevante en Java (y C #) es "las subexpresiones se evalúan de izquierda a derecha". Dado que A () aparece a la izquierda de C (), A () se evalúa primero, independientemente del hecho de que C () esté involucrado en una multiplicación y A () esté involucrado solo en una suma.

Entonces ahora tiene suficiente información para responder su pregunta. En a[b] = b = 0las reglas de asociatividad decimos que esto es a[b] = (b = 0);pero eso no significa que el b=0primero corre! Las reglas de precedencia dicen que la indexación tiene una precedencia mayor que la asignación, pero eso no significa que el indexador se ejecute antes de la asignación de más a la derecha .

(ACTUALIZACIÓN: una versión anterior de esta respuesta tenía algunas omisiones pequeñas y prácticamente sin importancia en la sección que sigue que he corregido. También escribí un artículo de blog que describe por qué estas reglas son sensatas en Java y C # aquí: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

La precedencia y la asociatividad solo nos dicen que la asignación de cero a bdebe ocurrir antes de la asignación de a[b], porque la asignación de cero calcula el valor que se asigna en la operación de indexación. Precedencia y asociatividad solos no dicen nada sobre si el a[b]se evalúa antes o después de la b=0.

Nuevamente, esto es lo mismo que: A()[B()] = C()- Todo lo que sabemos es que la indexación debe realizarse antes de la asignación. No sabemos si A (), B () o C () se ejecuta primero en función de la precedencia y la asociatividad . Necesitamos otra regla que nos diga eso.

La regla es, nuevamente, "cuando tenga la opción de qué hacer primero, vaya siempre de izquierda a derecha". Sin embargo, hay una arruga interesante en este escenario específico. ¿El efecto secundario de una excepción lanzada causado por una colección nula o un índice fuera de rango se considera parte del cálculo del lado izquierdo de la asignación o parte del cálculo de la asignación en sí? Java elige este último. (Por supuesto, esta es una distinción que solo importa si el código ya es incorrecto , porque el código correcto no elimina la referencia nula ni pasa un índice incorrecto en primer lugar).

¿Así que lo que pasa?

  • El a[b]está a la izquierda de b=0, por lo que se a[b]ejecuta primero , lo que da como resultado a[1]. Sin embargo, la verificación de la validez de esta operación de indexación se retrasa.
  • Entonces b=0sucede.
  • Entonces ocurre la verificación que aes válida y a[1]está dentro del rango
  • La asignación del valor a[1]ocurre en último lugar.

Entonces, aunque en este caso específico hay algunas sutilezas a considerar para esos raros casos de error que no deberían ocurrir en el código correcto en primer lugar, en general puede razonar: las cosas de la izquierda suceden antes que las de la derecha . Esa es la regla que estás buscando. Hablar de precedencia y asociatividad es confuso e irrelevante.

La gente se equivoca todo el tiempo en estas cosas , incluso las que deberían saberlo mejor. He editado demasiados libros de programación que establecían las reglas incorrectamente, por lo que no es de extrañar que mucha gente tenga creencias completamente incorrectas sobre la relación entre precedencia / asociatividad y orden de evaluación, es decir, que en realidad no existe tal relación ; son independientes.

Si este tema le interesa, vea mis artículos sobre el tema para leer más:

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

Se trata de C #, pero la mayoría de estas cosas se aplican igualmente bien a Java.

Eric Lippert
fuente
6
Personalmente prefiero el modelo mental donde en el primer paso construyes un árbol de expresión usando precedencia y asociatividad. Y en el segundo paso evalúe de forma recursiva ese árbol comenzando por la raíz. Con la evaluación de un nodo: Evalúe los nodos secundarios inmediatos de izquierda a derecha y luego la nota misma. | Una ventaja de este modelo es que maneja trivialmente el caso en el que los operadores binarios tienen un efecto secundario. Pero la principal ventaja es que simplemente se adapta mejor a mi cerebro.
CodesInChaos
¿Estoy en lo cierto de que C ++ no garantiza esto? ¿Y Python?
Neil G
2
@Neil: C ++ no garantiza nada sobre el orden de evaluación, y nunca lo hizo. (Tampoco C.) Python lo garantiza estrictamente por orden de precedencia; a diferencia de todo lo demás, la asignación es R2L.
Donal Fellows
2
@aroth para mí suenas simplemente confundido. Y las reglas de precedencia solo implican que los niños deben ser evaluados antes que los padres. Pero no dicen nada sobre el orden en que se evalúa a los niños. Java y C # eligieron de izquierda a derecha, C y C ++ eligieron un comportamiento indefinido.
CodesInChaos
6
@noober: Bien, considere: M (A () + B (), C () * D (), E () + F ()). ¿Su deseo es que las subexpresiones se evalúen en qué orden? ¿Deben evaluarse C () y D () antes que A (), B (), E () y F () porque la multiplicación tiene mayor precedencia que la suma? Es fácil decir que "obviamente" el orden debería ser diferente. Es bastante más difícil llegar a una regla real que cubra todos los casos. Los diseñadores de C # y Java eligieron una regla simple y fácil de explicar: "ir de izquierda a derecha". ¿Cuál es su reemplazo propuesto y por qué cree que su regla es mejor?
Eric Lippert
32

No obstante, la magistral respuesta de Eric Lippert no es de la debida ayuda porque habla de un idioma diferente. Esto es Java, donde Java Language Specification es la descripción definitiva de la semántica. En particular, §15.26.1 es relevante porque describe el orden de evaluación para el =operador (todos sabemos que es asociativo a la derecha, ¿no?). Reduciéndolo un poco a los bits que nos importan en esta pregunta:

Si la expresión del operando de la izquierda es una expresión de acceso a una matriz ( §15.13 ), se requieren muchos pasos:

  • Primero, se evalúa la subexpresión de referencia a la matriz de la expresión de acceso a la matriz del operando de la izquierda. Si esta evaluación se completa abruptamente, entonces la expresión de asignación se completa abruptamente por la misma razón; la subexpresión de índice (de la expresión de acceso a la matriz de operandos de la izquierda) y el operando de la derecha no se evalúan y no se produce ninguna asignación.
  • De lo contrario, se evalúa la subexpresión de índice de la expresión de acceso a la matriz de operandos de la izquierda. Si esta evaluación se completa abruptamente, entonces la expresión de asignación se completa abruptamente por la misma razón y el operando de la derecha no se evalúa y no ocurre ninguna asignación.
  • De lo contrario, se evalúa el operando de la derecha. Si esta evaluación se completa abruptamente, entonces la expresión de asignación se completa abruptamente por la misma razón y no ocurre ninguna asignación.

[… Luego pasa a describir el significado real de la tarea en sí, que podemos ignorar aquí por brevedad…]

En resumen, Java tiene un orden de evaluación muy definido que es exactamente de izquierda a derecha dentro de los argumentos de cualquier operador o llamada de método. Las asignaciones de matrices son uno de los casos más complejos, pero incluso allí sigue siendo L2R. (El JLS recomienda que no escriba código que necesite este tipo de restricciones semánticas complejas , y yo también: ¡puede meterse en problemas más que suficientes con solo una asignación por declaración!)

C y C ++ son definitivamente diferentes a Java en esta área: sus definiciones de lenguaje dejan el orden de evaluación sin definir deliberadamente para permitir más optimizaciones. Aparentemente, C # es como Java, pero no conozco su literatura lo suficiente como para poder señalar la definición formal. (Sin embargo, esto realmente varía según el idioma, Ruby es estrictamente L2R, al igual que Tcl, aunque carece de un operador de asignación per se por razones que no son relevantes aquí, y Python es L2R pero R2L con respecto a la asignación , lo cual me parece extraño, pero ya está. .)

Becarios Donales
fuente
11
Entonces, ¿lo que estás diciendo es que la respuesta de Eric es incorrecta porque Java lo define específicamente para ser exactamente lo que dijo?
configurador
8
La regla relevante en Java (y C #) es "las subexpresiones se evalúan de izquierda a derecha" . Me parece que está hablando de ambos.
configurador
2
Un poco confuso aquí: ¿esto hace que la respuesta anterior de Eric Lippert sea menos cierta, o solo está citando una referencia específica de por qué es cierta?
GreenieMeanie
5
@Greenie: La respuesta de Eric es cierta, pero como dije, no puedes tomar una idea de un idioma en esta área y aplicarla a otro sin tener cuidado. Entonces cité la fuente definitiva.
Donal Fellows
1
lo curioso es que la derecha se evalúa antes de que se resuelva la variable de la izquierda; in a[-1]=c, cse evalúa, before -1se reconoce como inválido.
ZhongYu
5
a[b] = b = 0;

1) el operador de indexación de matriz tiene mayor prioridad que el operador de asignación (consulte esta respuesta ):

(a[b]) = b = 0;

2) Según 15.26. Operadores de asignación de JLS

Hay 12 operadores de asignación; todos son sintácticamente asociativos por la derecha (se agrupan de derecha a izquierda). Por lo tanto, a = b = c significa a = (b = c), que asigna el valor de c a b y luego asigna el valor de b a a.

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

3) Según 15.7. Orden de evaluación de JLS

El lenguaje de programación Java garantiza que los operandos de los operadores parecen ser evaluados en un orden de evaluación específico, es decir, de izquierda a derecha.

y

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

Entonces:

a) (a[b])evaluado primero ena[1]

b) luego (b=0)evaluado para0

c) (a[1] = 0)evaluado en último lugar

bup
fuente
1

Tu código es equivalente a:

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

lo que explica el resultado.

Jérôme Verstrynge
fuente
7
La pregunta es por qué es así.
Mat
@Mat La respuesta es porque esto es lo que sucede bajo el capó considerando el código proporcionado en la pregunta. Así es como ocurre la evaluación.
Jérôme Verstrynge
1
Sí, lo sé. Todavía no respondo la pregunta a través de la OMI, razón por la cual así es como ocurre la evaluación.
Mat
1
@Mat '¿Por qué es así como ocurre la evaluación?' no es la pregunta formulada. '¿Cómo sucedió la evaluación?' es la pregunta formulada.
Jérôme Verstrynge
1
@JVerstry: ¿Cómo no son equivalentes? La subexpresión de referencia de matriz del operando de la izquierda es el operando más a la izquierda. Por lo tanto, decir "hacer lo que está más a la izquierda primero" es exactamente lo mismo que decir "hacer primero la referencia a la matriz". Si los autores de especificaciones de Java optaron por ser innecesariamente prolijos y redundantes al explicar esta regla en particular, bien por ellos; este tipo de cosas es confuso y debería ser más en lugar de menos prolijo. Pero no veo cómo mi caracterización concisa es semánticamente diferente de su caracterización prolija.
Eric Lippert
0

Considere otro ejemplo más detallado a continuación.

Como regla general:

Es mejor tener una tabla de las reglas de orden de precedencia y asociatividad disponible para leer al resolver estas preguntas, por ejemplo http://introcs.cs.princeton.edu/java/11precedence/

He aquí un buen ejemplo:

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

Pregunta: ¿Cuál es la salida de la línea anterior?

Respuesta: Aplicar las reglas de precedencia y asociatividad

Paso 1: De acuerdo con las reglas de precedencia: los operadores / y * tienen prioridad sobre los operadores + -. Por lo tanto, el punto de partida para ejecutar esta ecuación se reducirá a:

100/10*2

Paso 2: De acuerdo con las reglas y la precedencia: / y * son iguales en precedencia.

Como los operadores / y * tienen la misma precedencia, debemos observar la asociatividad entre esos operadores.

De acuerdo con las REGLAS DE ASOCIATIVIDAD de estos dos operadores en particular, comenzamos a ejecutar la ecuación de IZQUIERDA A DERECHA, es decir, 100/10 se ejecuta primero:

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

Paso 3: La ecuación se encuentra ahora en el siguiente estado de ejecución:

=3+20-13

De acuerdo con las reglas y la precedencia: + y - son iguales en precedencia.

Ahora debemos observar la asociatividad entre los operadores + y - operadores. De acuerdo con la asociatividad de estos dos operadores particulares, comenzamos a ejecutar la ecuación de IZQUIERDA a DERECHA, es decir, se ejecuta primero 3 + 20:

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

10 es la salida correcta cuando se compila

Nuevamente, es importante tener una tabla de las reglas de orden de precedencia y asociatividad con usted al resolver estas preguntas, por ejemplo, http://introcs.cs.princeton.edu/java/11precedence/

Mark Burleigh
fuente
1
Está diciendo que la "asociatividad entre los operadores + y - operadores" es "DE DERECHA A IZQUIERDA". Trate de usar esa lógica y evalúe 10 - 4 - 3.
Pshemo
1
Sospecho que este error puede ser causado por el hecho de que en la parte superior de introcs.cs.princeton.edu/java/11precedence + hay un operador unario (que tiene asociatividad de derecha a izquierda), pero los aditivos + y - tienen como multiplicativo * /% left a la asociatividad derecha.
Pshemo
Bien identificado y modificado en consecuencia, gracias Pshemo
Mark Burleigh
Esta respuesta explica la precedencia y la asociatividad, pero, como explicó Eric Lippert , la pregunta es sobre el orden de evaluación, que es muy diferente. De hecho, esto no responde a la pregunta.
Fabio dice Reincorporar a Monica