java - tiempo - ¿Por qué esto entra en un bucle infinito?
ciclo while (27)
Tengo el siguiente código:
public class Tests {
public static void main(String[] args) throws Exception {
int x = 0;
while(x<3) {
x = x++;
System.out.println(x);
}
}
}
Sabemos que debería haber escrito solo x++
o x=x+1
, pero en x = x++
primero debe atribuirse x
, y luego incrementarlo. ¿Por qué x
continúa con 0
como valor?
--actualizar
Aquí está el bytecode:
public class Tests extends java.lang.Object{
public Tests();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iconst_3
4: if_icmpge 22
7: iload_1
8: iinc 1, 1
11: istore_1
12: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_1
16: invokevirtual #3; //Method java/io/PrintStream.println:(I)V
19: goto 2
22: return
}
Voy a leer acerca de las instructions para tratar de entender ...
- La notación de prefijo incrementará la variable ANTES de evaluar la expresión.
- La notación de Postfix incrementará DESPUÉS de la evaluación de la expresión.
Sin embargo, " =
" tiene una prioridad de operador más baja que " ++
".
Entonces x=x++;
debe evaluar de la siguiente manera
-
x
preparado para la tarea (evaluado) -
x
incrementado - Valor anterior de
x
asignado ax
.
Antes de incrementar el valor en uno, el valor se asigna a la variable.
Bueno, por lo que puedo ver, el error se produce debido a que la asignación anula el valor incrementado, con el valor anterior al incremento, es decir, deshace el incremento.
Específicamente, la expresión "x ++" tiene el valor de ''x'' antes del incremento en lugar de "++ x", que tiene el valor de ''x'' después del incremento.
Si está interesado en investigar el código de byte, veremos las tres líneas en cuestión:
7: iload_1
8: iinc 1, 1
11: istore_1
7: iload_1 # pondrá el valor de la segunda variable local en la pila
8: iinc 1,1 # incrementará la segunda variable local con 1, tenga en cuenta que deja la pila sin tocar.
9: istore_1 # abrirá la parte superior de la pila y guardará el valor de este elemento en la segunda variable local
(Puedes leer los efectos de cada instrucción JVM here )
Esta es la razón por la que el código anterior se repetirá indefinidamente, mientras que la versión con ++ x no. El bytecode para ++ x debería verse bastante diferente, por lo que recuerdo del compilador 1.3 Java que escribí hace poco más de un año, el bytecode debería ser algo como esto:
iinc 1,1
iload_1
istore_1
Entonces, solo intercambiando las dos primeras líneas, cambia la semántica para que el valor que queda en la parte superior de la pila, después del incremento (es decir, el ''valor'' de la expresión) sea el valor después del incremento.
Compruebe el siguiente código,
int x=0;
int temp=x++;
System.out.println("temp = "+temp);
x = temp;
System.out.println("x = "+x);
la salida será,
temp = 0
x = 0
post increment
significa incrementar el valor y devolver el valor antes del incremento . Es por eso que el valor temp
es 0
. Entonces, ¿qué pasa si temp = i
y esto está en un bucle (a excepción de la primera línea de código). Al igual que en la pregunta !!!!
Creo que porque en Java ++ tiene una prioridad más alta que = (asignación) ... ¿Lo tiene? Mire http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html ...
De la misma manera, si escribe x = x + 1 ... + tiene una prioridad más alta que = (asignación)
Cuando ++ está en el rhs, el resultado se devuelve antes de que se incremente el número. Cambia a ++ x y hubiera estado bien. Java habría optimizado esto para realizar una sola operación (la asignación de x a x) en lugar del incremento.
De http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.html
Los operadores de incremento / decremento pueden aplicarse antes (prefijo) o después (postfix) del operando. El resultado del código ++; y ++ resultado; ambos terminarán en un resultado incrementado en uno. La única diferencia es que la versión del prefijo (resultado de ++) se evalúa al valor incrementado, mientras que la versión de postfix (resultado ++) se evalúa al valor original . Si solo está realizando un incremento / decremento simple, no importa qué versión elija. Pero si usa este operador en parte de una expresión más grande, el que elija puede hacer una diferencia significativa.
Para ilustrar, prueba lo siguiente:
int x = 0;
int y = 0;
y = x++;
System.out.println(x);
System.out.println(y);
El cual imprimirá 1 y 0.
El operador de incremento se aplica a la misma variable que le está asignando. Eso es pedir problemas. Estoy seguro de que puedes ver el valor de tu variable x mientras ejecutas este programa ... eso debería dejar claro por qué el ciclo nunca termina.
El valor permanece en 0 porque el valor de x++
es 0. En este caso, no importa si el valor de x
aumenta o no, la asignación x=0
se ejecuta. Esto sobrescribirá el valor incrementado temporal de x
(que fue 1 durante un "tiempo muy corto").
Está sucediendo porque se ha incrementado la publicación. Significa que la variable se incrementa después de evaluar la expresión.
int x = 9;
int y = x++;
x ahora es 10, pero y es 9, el valor de x antes de que se incrementara.
Ver más en Definición de Incremento de Publicaciones .
Estás obteniendo efectivamente el siguiente comportamiento.
- toma el valor de x (que es 0) como "el resultado" del lado derecho
- incrementa el valor de x (entonces x es 1)
- asigne el resultado del lado derecho (que se guardó como 0) a x (x ahora es 0)
La idea es que el operador post-incremento (x ++) incremente esa variable en cuestión DESPUÉS de devolver su valor para su uso en la ecuación en la que se usa.
Edición: Añadiendo un poco debido al comentario. Considérelo como el siguiente.
x = 1; // x == 1
x = x++ * 5;
// First, the right hand side of the equation is evaluated.
==> x = 1 * 5;
// x == 2 at this point, as it "gave" the equation its value of 1
// and then gets incremented by 1 to 2.
==> x = 5;
// And then that RightHandSide value is assigned to
// the LeftHandSide variable, leaving x with the value of 5.
Esta declaración:
x = x++;
evalúa así:
- Empuje
x
en la pila; - Incremento
x
; - Pop
x
de la pila.
Así que el valor no ha cambiado. Compara eso con:
x = ++x;
que evalúa como:
- Incremento
x
; - Empuje
x
en la pila; - Pop
x
de la pila.
Lo que quieres es:
while (x < 3) {
x++;
System.out.println(x);
}
Esto es porque nunca se incrementa en este caso. x++
usará su valor primero antes de incrementar, como en este caso, será como:
x = 0;
Pero si lo haces ++x;
esto aumentará.
Esto funciona como esperas que el otro lo haga. Es la diferencia entre prefijo y postfix.
int x = 0;
while (x < 3) x = (++x);
Esto sucede porque el valor de x
no se incrementa en absoluto.
x = x++;
es equivalente a
int temp = x;
x++;
x = temp;
Explicación:
Veamos el código de bytes para esta operación. Considere una clase de muestra:
class test {
public static void main(String[] args) {
int i=0;
i=i++;
}
}
Ahora ejecutando el desensamblador de clase en esto obtenemos:
$ javap -c test
Compiled from "test.java"
class test extends java.lang.Object{
test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: return
}
Ahora, la Java VM está basada en la pila, lo que significa que para cada operación, los datos se insertarán en la pila y de la pila, los datos se desplegarán para realizar la operación. También hay otra estructura de datos, normalmente una matriz para almacenar las variables locales. Las variables locales reciben identificadores que son solo los índices de la matriz.
Veamos los mnemonics en el método main()
:
-
iconst_0
: el valor constante0
se inserta en la pila. -
istore_1
: el elemento superior de la pila se extrae y se almacena en la variable local con el índice1
que esx
. -
iload_1
: el valor en la ubicación1
que es el valor dex
que es0
, se inserta en la pila. -
iinc 1, 1
: El valor en la ubicación de memoria1
se incrementa en1
. Entoncesx
ahora se convierte en1
. -
istore_1
: el valor en la parte superior de la pila se almacena en la ubicación de memoria1
. Eso es0
se asigna ax
sobrescribiendo su valor incrementado.
Por lo tanto, el valor de x
no cambia resultando en el bucle infinito.
Frase
x = x++;
"traduce" a
x = x;
x = x + 1;
Eso es.
La expresión x++
evalúa como x
. La parte ++
afecta el valor después de la evaluación , no después de la declaración . entonces x = x++
se traduce efectivamente a
int y = x; // evaluation
x = x + 1; // increment part
x = y; // assignment
La respuesta es bastante sencilla. Tiene que ver con el orden en que se evalúan las cosas. x++
devuelve el valor x
luego incrementa x
.
En consecuencia, el valor de la expresión x++
es 0
. Entonces, estás asignando x=0
cada vez en el bucle. Ciertamente x++
incrementa este valor, pero eso sucede antes de la asignación.
Me pregunto si hay algo en la especificación de Java que defina con precisión el comportamiento de esto. (La implicación obvia de esa afirmación es que soy demasiado perezoso para comprobarlo).
Nota del código de bytes de Tom, las líneas clave son 7, 8 y 11. La línea 7 carga x en la pila de cómputo. Línea 8 incrementos x. La línea 11 almacena el valor de la pila de nuevo a x. En los casos normales en los que no se asignan valores a sí mismos, no creo que haya ninguna razón por la que no pueda cargar, almacenar y luego incrementar. Obtendrías el mismo resultado.
Supongamos que tienes un caso más normal en el que escribiste algo como: z = (x ++) + (y ++);
Si lo dijo (pseudocódigo para saltar tecnicismos)
load x
increment x
add y
increment y
store x+y to z
o
load x
add y
store x+y to z
increment x
increment y
debe ser irrelevante. Cualquiera de las dos implementaciones debería ser válida, pensaría yo.
Sería extremadamente cauteloso al escribir código que depende de este comportamiento. A mi me parece muy dependiente de la implementación, entre las grietas de las especificaciones. La única vez que haría una diferencia es si hiciera algo alocado, como el ejemplo aquí, o si tuviera dos subprocesos en ejecución y dependiera del orden de evaluación dentro de la expresión.
Ninguna de las respuestas fue muy acertada, así que aquí va:
Cuando escribe int x = x++
, no está asignando x
para ser él mismo en el nuevo valor, está asignando x
para que sea el valor de retorno de la expresión x++
. Cuál es el valor original de x
, como se indicó en la respuesta de Colin Cochrane .
Por diversión, prueba el siguiente código:
public class Autoincrement {
public static void main(String[] args) {
int x = 0;
System.out.println(x++);
System.out.println(x);
}
}
El resultado será
0
1
El valor de retorno de la expresión es el valor inicial de x
, que es cero. Pero más adelante, cuando leemos el valor de x
, recibimos el valor actualizado, que es uno.
Piense en x ++ como una llamada de función que "devuelve" lo que X era antes del incremento (por eso se llama post-incremento).
Así que la orden de operación es:
1: almacena en caché el valor de x antes de incrementar
2: incremento x
3: devolver el valor en caché (x antes de que se incrementó)
4: el valor de retorno se asigna a x
Realmente no necesita el código de máquina para entender lo que está pasando.
Según las definiciones:
El operador de asignación evalúa la expresión del lado derecho y la almacena en una variable temporal.
1.1. El valor actual de x se copia en esta variable temporal
1.2. x se incrementa ahora.
La variable temporal se copia en el lado izquierdo de la expresión, que es x por casualidad. Por eso es que el antiguo valor de x se copia nuevamente en sí mismo.
Es bastante simple.
Ya ha sido bien explicado por otros. Acabo de incluir los enlaces a las secciones relevantes de la especificación de Java.
x = x ++ es una expresión. Java seguirá el orden de evaluación . Primero evaluará la expresión x ++, que incrementará x y establecerá el valor del resultado en el valor anterior de x . Luego asignará el resultado de la expresión a la variable x. Al final, x vuelve a su valor anterior.
x = x++
funciona de la siguiente manera:
- Primero evalúa la expresión
x++
. La evaluación de esta expresión produce un valor de expresión (que es el valor dex
antes del incremento) e incrementax
. - Más tarde asigna el valor de la expresión a
x
, sobrescribiendo el valor incrementado.
Por lo tanto, la secuencia de eventos parece ser la siguiente (es un código de bytes descompilado real, según lo producido por javap -c
, con mis comentarios):
8: iload_1 // Remember current value of x in the stack 9: iinc 1, 1 // Increment x (doesn''t change the stack) 12: istore_1 // Write remebered value from the stack to x
Para comparación, x = ++x
:
8: iinc 1, 1 // Increment x 11: iload_1 // Push value of x onto stack 12: istore_1 // Pop value from the stack to x
Nota : Originalmente publiqué el código C # en esta respuesta con fines ilustrativos, ya que C # le permite pasar parámetros int
por referencia con la palabra clave ref
. Decidí actualizarlo con el código legal real de Java usando la primera clase MutableInt
que encontré en Google para determinar aproximadamente lo que hace la ref
en C #. Realmente no puedo decir si eso ayuda o lastima la respuesta. Diré que personalmente no he hecho todo el desarrollo de Java; así que, por lo que sé, podría haber maneras mucho más idiomáticas de ilustrar este punto.
Quizás si escribimos un método para hacer el equivalente de lo que x++
hace, esto lo hará más claro.
public MutableInt postIncrement(MutableInt x) {
int valueBeforeIncrement = x.intValue();
x.add(1);
return new MutableInt(valueBeforeIncrement);
}
¿Derecha? Incremente el valor pasado y devuelva el valor original: esa es la definición del operador de postincremento.
Ahora, veamos cómo se comporta este comportamiento en su código de ejemplo:
MutableInt x = new MutableInt();
x = postIncrement(x);
postIncrement(x)
hace que? Incrementos x
, si. Y luego devuelve lo que x
era antes del incremento . Este valor de retorno entonces se asigna a x
.
Entonces el orden de los valores asignados a x
es 0, luego 1, luego 0.
Esto podría ser aún más claro si reescribimos lo anterior:
MutableInt x = new MutableInt(); // x is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
x = temp; // Now x is 0 again.
Su fijación en el hecho de que cuando reemplaza x
en el lado izquierdo de la asignación anterior con y
, "puede ver que primero aumenta x, y luego lo atribuye a y" me parece confuso. No es x
que se asigna a y
; es el valor asignado anteriormente a x
. Realmente, inyectar y
hace que las cosas no sean diferentes del escenario anterior; simplemente tenemos:
MutableInt x = new MutableInt(); // x is 0.
MutableInt y = new MutableInt(); // y is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
y = temp; // y is still 0.
Entonces está claro: x = x++
efectivamente no cambia el valor de x. Siempre hace que x tenga los valores x 0 , luego x 0 + 1, y luego x 0 nuevamente.
Actualización : Por cierto, para que no dude que x
alguna vez se asigna a 1 "entre" la operación de incremento y la asignación en el ejemplo anterior, he presentado una demostración rápida para ilustrar que este valor intermedio "existe", aunque nunca será "visto" en el hilo de ejecución.
La demo llama a x = x++;
en un bucle mientras un hilo separado imprime continuamente el valor de x
en la consola.
public class Main {
public static volatile int x = 0;
public static void main(String[] args) {
LoopingThread t = new LoopingThread();
System.out.println("Starting background thread...");
t.start();
while (true) {
x = x++;
}
}
}
class LoopingThread extends Thread {
public @Override void run() {
while (true) {
System.out.println(Main.x);
}
}
}
A continuación se muestra un extracto de la salida del programa anterior. Note la ocurrencia irregular de 1s y 0s.
Starting background thread... 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1
x++
=: (x = x + 1) - 1
Asi que:
x = x++;
=> x = ((x = x + 1) - 1)
=> x = ((x + 1) - 1)
=> x = x; // Doesn''t modify x!
Mientras
++x
=: x = x + 1
Asi que:
x = ++x;
=> x = (x = x + 1)
=> x = x + 1; // Increments x
Por supuesto, el resultado final es el mismo que solo x++;
o ++x;
en una línea por sí misma.
x = x++; (increment is overriden by = )
debido a la declaración anterior, x nunca llega a 3;