Por que isso entra em um loop infinito?

Eu tenho o seguinte 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 ele deveria ter escrito apenas x++ ou x=x+1 , mas em x = x++ ele deveria primeiro atribuir x a si mesmo, e depois incrementá-lo. Por que x continua com 0 como valor?

–atualizar

Aqui está o bytecode:

 public class Tests extends java.lang.Object{ public Tests(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()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 } 

Eu vou ler sobre as instruções para tentar entender …

Nota : Originalmente eu postei o código C # nesta resposta para fins de ilustração, uma vez que o C # permite que você passe parâmetros int por referência com a palavra-chave ref . Decidi atualizá-lo com código Java legal real usando a primeira class MutableInt que encontrei no Google para classificar o que ref faz em C #. Eu não posso dizer se isso ajuda ou fere a resposta. Eu vou dizer que eu pessoalmente não fiz tanto desenvolvimento em Java; então, pelo que sei, pode haver maneiras muito mais idiomáticas de ilustrar esse ponto.


Talvez, se escrevermos um método para fazer o equivalente ao que o x++ faz, isso ficará mais claro.

 public MutableInt postIncrement(MutableInt x) { int valueBeforeIncrement = x.intValue(); x.add(1); return new MutableInt(valueBeforeIncrement); } 

Certo? Incrementar o valor passado e retornar o valor original: essa é a definição do operador pós-incremento.

Agora, vamos ver como esse comportamento acontece no seu código de exemplo:

 MutableInt x = new MutableInt(); x = postIncrement(x); 

postIncrement(x) faz o que? Incrementos x , sim. E então retorna o que x era antes do incremento . Este valor de retorno é então atribuído a x .

Portanto, a ordem dos valores atribuídos a x é 0, depois 1 e 0.

Isso pode ficar mais claro ainda se rewritemos o que foi dito acima:

 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. 

Sua fixação no fato de que quando você substitui x no lado esquerdo da atribuição acima com y , “você pode ver que primeiro incrementa x, e depois atribui a y” parece-me confuso. Não é x que está sendo atribuído a y ; é o valor anteriormente atribuído a x . Realmente, injetar y torna as coisas não diferentes do cenário acima; nós simplesmente temos:

 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. 

Então está claro: x = x++ efetivamente não altera o valor de x. Isso sempre faz com que x tenha os valores x 0 , depois x 0 + 1 e, em seguida, x 0 novamente.


Atualização : Incidentalmente, para que você não duvide que x seja atribuído a 1 “entre” a operação de incremento e a atribuição no exemplo acima, eu juntei uma demo rápida para ilustrar que esse valor intermediário realmente “existe”, embora nunca será “visto” no encadeamento de execução.

A demonstração chama x = x++; em um loop enquanto um thread separado imprime continuamente o valor de x para o console.

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

Abaixo está um trecho da saída do programa acima. Observe a ocorrência irregular de 1s e 0s.

 Começando o segundo plano ...
 0
 0
 1
 1
 0
 0
 0
 0
 0
 0
 0
 0
 0
 0
 1
 0
 1

x = x++ funciona da seguinte maneira:

  • Primeiro avalia a expressão x++ . A avaliação dessa expressão produz um valor de expressão (que é o valor de x antes do incremento) e incrementa x .
  • Posteriormente, atribui o valor da expressão a x , sobrescrevendo o valor incrementado.

Então, a sequência de events se parece com a seguinte (é um bytecode decompilado, como produzido pelo javap -c , com meus comentários):

  8: iload_1 // Lembre-se do valor atual de x na pilha
    9: iinc 1, 1 // Incremento x (não altera a pilha)
    12: istore_1 // Escreve o valor remebered da pilha para x

Para comparação, x = ++x :

  8: iinc 1, 1 // Incremento x
    11: iload_1 // Pressione o valor de x na pilha
    12: istore_1 // Valor pop da pilha para x 

Isso acontece porque o valor de x não é incrementado.

 x = x++; 

é equivalente a

 int temp = x; x++; x = temp; 

Explicação:

Vamos ver o código de bytes dessa operação. Considere uma class de amostra:

 class test { public static void main(String[] args) { int i=0; i=i++; } } 

Agora executando o desassemblador de class sobre isso, obtemos:

 $ 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."":()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 } 

Agora, a VM Java é baseada em pilha, o que significa que, para cada operação, os dados serão colocados na pilha e, a partir da pilha, os dados serão exibidos para executar a operação. Há também outra estrutura de dados, geralmente uma matriz para armazenar as variables ​​locais. As variables ​​locais recebem ids que são apenas os índices para o array.

Vamos olhar para os mnemônicos no método main() :

  • iconst_0 : O valor constante 0 é empurrado para a pilha.
  • istore_1 : O elemento principal da pilha é exibido e armazenado na variável local com índice 1
    qual é x .
  • iload_1 : O valor no local 1 que é o valor de x que é 0 , é colocado na pilha.
  • iinc 1, 1 : O valor no local de memory 1 é incrementado em 1 . Então x agora se torna 1 .
  • istore_1 : O valor no topo da pilha é armazenado no local da memory 1 . Isso é 0 é atribuído a x sobrescrevendo seu valor incrementado.

Portanto, o valor de x não muda, resultando no loop infinito.

  1. A notação de prefixo aumentará a variável ANTES que a expressão seja avaliada.
  2. A notação do postfix incrementará APÓS a avaliação da expressão.

No entanto, ” = ” tem uma precedência de operador menor que ” ++ “.

Então x=x++; deve avaliar da seguinte forma

  1. x preparado para atribuição (avaliado)
  2. x incrementado
  3. Valor anterior de x atribuído a x .

Nenhuma das respostas foi bem visível, então aqui vai:

Quando você está escrevendo int x = x++ , você não está atribuindo x para ser ele mesmo no novo valor, você está atribuindo x para ser o valor de retorno da expressão x++ . Qual é o valor original de x , como sugerido na resposta de Colin Cochrane .

Por diversão, teste o seguinte código:

 public class Autoincrement { public static void main(String[] args) { int x = 0; System.out.println(x++); System.out.println(x); } } 

O resultado será

 0 1 

O valor de retorno da expressão é o valor inicial de x , que é zero. Porém, mais tarde, ao ler o valor de x , recebemos o valor atualizado, que é um.

Já foi bem explicado por outro. Eu apenas incluo os links para as seções relevantes da especificação Java.

x = x ++ é uma expressão. Java seguirá a ordem de avaliação . Primeiramente, ele avaliará a expressão x ++, que incrementará x e definirá o valor do resultado para o valor anterior de x . Em seguida, ele atribuirá o resultado da expressão à variável x. No final, x está de volta ao seu valor anterior.

Esta afirmação:

 x = x++; 

avalia assim:

  1. Empurre x na pilha;
  2. Incremento x ;
  3. Pop x da pilha.

Então o valor é inalterado. Compare isso com:

 x = ++x; 

que avalia como:

  1. Incremento x ;
  2. Empurre x na pilha;
  3. Pop x da pilha.

O que você quer é:

 while (x < 3) { x++; System.out.println(x); } 

A resposta é bem direta. Tem a ver com a ordem em que as coisas são avaliadas. x++ retorna o valor x seguida, incrementa x .

Consequentemente, o valor da expressão x++ é 0 . Então você está atribuindo x=0 cada vez no loop. Certamente x++ incrementa esse valor, mas isso acontece antes da atribuição.

De http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.html

Os operadores de incremento / decremento podem ser aplicados antes (prefixo) ou depois (postfix) do operando. O resultado do código ++; e ++ resultado; ambos terminarão em resultado sendo incrementado por um. A única diferença é que a versão do prefixo (resultado ++) é avaliada para o valor incrementado, enquanto a versão do postfix (resultado ++) é avaliada para o valor original . Se você está apenas executando um incremento / decremento simples, não importa qual versão você escolher. Mas se você usar esse operador em parte de uma expressão maior, o que você escolher poderá fazer uma diferença significativa.

Para ilustrar, tente o seguinte:

  int x = 0; int y = 0; y = x++; System.out.println(x); System.out.println(y); 

Qual imprimirá 1 e 0.

Você está efetivamente recebendo o seguinte comportamento.

  1. pegue o valor de x (que é 0) como “o resultado” do lado direito
  2. incrementa o valor de x (então x é agora 1)
  3. atribuir o resultado do lado direito (que foi salvo como 0) para x (x é agora 0)

A ideia é que o operador pós-incremento (x ++) incrementa essa variável na questão AFTER retornando seu valor para uso na equação em que é usada.

Edit: Adicionando um leve pouco por causa do comentário. Considere como o seguinte.

 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. 

Você não precisa realmente do código da máquina para entender o que está acontecendo.

De acordo com as definições:

  1. O operador de atribuição avalia a expressão do lado direito e a armazena em uma variável temporária.

    1.1. O valor atual de x é copiado para esta variável temporária

    1.2. x é incrementado agora.

  2. A variável temporária é então copiada para o lado esquerdo da expressão, que é x por acaso! É por isso que o valor antigo de x é novamente copiado para si mesmo.

É bem simples.

Isso ocorre porque nunca é incrementado nesse caso. x++ usará o valor primeiro antes de incrementar como neste caso será como:

 x = 0; 

Mas se você fizer ++x; isso aumentará.

O valor permanece em 0 porque o valor de x++ é 0. Neste caso, não importa se o valor de x é aumentado ou não, a atribuição x=0 é executada. Isso sobrescreverá o valor incremental temporário de x (que foi 1 para um “tempo muito curto”).

Isso funciona como você espera que o outro faça. É a diferença entre o prefixo e o postfix.

 int x = 0; while (x < 3) x = (++x); 

Pense em x ++ como uma chamada de function que “retorna” o que o X era antes do incremento (é por isso que ele é chamado de pós-incremento).

Portanto, a ordem de operação é:
1: cache o valor de x antes de incrementar
2: incremento x
3: retorna o valor em cache (x antes de ser incrementado)
4: valor de retorno é atribuído a x

Quando o ++ está no rhs, o resultado é retornado antes que o número seja incrementado. Mude para ++ x e estaria tudo bem. Java teria otimizado isso para executar uma única operação (a atribuição de x para x) em vez do incremento.

Bem, tanto quanto eu posso ver, o erro ocorre, devido à atribuição substituindo o valor incrementado, com o valor antes da incremento, ou seja, desfaz o incremento.

Especificamente, a expressão “x ++”, tem o valor de ‘x’ antes do incremento, em oposição a “++ x”, que tem o valor de ‘x’ após a incremento.

Se você está interessado em investigar o bytecode, vamos dar uma olhada nas três linhas em questão:

  7: iload_1 8: iinc 1, 1 11: istore_1 

7: iload_1 # Colocará o valor da segunda variável local na pilha
8: iinc 1,1 # irá incrementar a segunda variável local com 1, note que ela deixa a pilha intacta!
9: istore_1 # Irromperá o topo da pilha e salvará o valor desse elemento na segunda variável local
(Você pode ler os efeitos de cada instrução da JVM aqui )

É por isso que o código acima irá fazer um loop indefinidamente, enquanto a versão com ++ x não irá. O bytecode para ++ x deve ser bem diferente, até onde eu me lembro do compilador Java 1.3 que eu escrevi há pouco mais de um ano, o bytecode deveria ser algo assim:

 iinc 1,1 iload_1 istore_1 

Então, apenas trocando as duas primeiras linhas, muda a semântica para que o valor deixado no topo da pilha, após o incremento (isto é, o ‘valor’ da expressão) seja o valor após o incremento.

  x++ =: (x = x + 1) - 1 

Assim:

  x = x++; => x = ((x = x + 1) - 1) => x = ((x + 1) - 1) => x = x; // Doesn't modify x! 

Enquanto que

  ++x =: x = x + 1 

Assim:

  x = ++x; => x = (x = x + 1) => x = x + 1; // Increments x 

Claro que o resultado final é o mesmo que x++; ou ++x; em uma linha por si só.

Frase

 x = x++; 

“traduz” para

 x = x; x = x + 1; 

É isso aí.

  x = x++; (increment is overriden by = ) 

por causa da declaração acima x nunca atinge 3;

Eu me pergunto se há alguma coisa na especificação Java que define precisamente o comportamento disso. (A implicação óbvia dessa afirmação é que eu tenho preguiça de checar.)

Nota do bytecode de Tom, as linhas de chave são 7, 8 e 11. A linha 7 carrega x na pilha de computação. Linha 8 incrementos x. A linha 11 armazena o valor da pilha de volta para x. Nos casos normais em que você não está atribuindo valores de volta para si, não acho que haveria qualquer motivo para não poder carregar, armazenar e incrementar. Você obteria o mesmo resultado.

Tipo, suponha que você tenha um caso mais normal em que escreveu algo como: z = (x ++) + (y ++);

Se disse (pseudocódigo para pular tecnicalidades)

 load x increment x add y increment y store x+y to z 

ou

 load x add y store x+y to z increment x increment y 

deve ser irrelevante. Qualquer implementação deve ser válida, eu acho.

Eu seria extremamente cauteloso sobre como escrever código que depende desse comportamento. Parece muito dependente da implementação, entre as rachaduras na especificação para mim. A única vez que isso faria diferença seria se você fizesse algo maluco, como o exemplo aqui, ou se você tivesse dois threads em execução e dependesse da ordem de avaliação dentro da expressão.

Eu acho que porque em Java + + tem uma precedência maior que = (atribuição) … Será que? Veja em http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html

Da mesma forma, se você escrever x = x + 1 … + tem uma precedência maior que = (atribuição)

A expressão x++ avaliada como x . A parte ++ afeta o valor após a avaliação , não após a instrução . então x = x++ é efetivamente traduzido em

 int y = x; // evaluation x = x + 1; // increment part x = y; // assignment 

Antes de incrementar o valor em um, o valor é atribuído à variável.

Está acontecendo porque é pós-incrementado. Isso significa que a variável é incrementada após a expressão ser avaliada.

 int x = 9; int y = x++; 

x é agora 10, mas y é 9, o valor de x antes de ser incrementado.

Veja mais em Definição de Pós Incremento .

Verifique o código abaixo,

  int x=0; int temp=x++; System.out.println("temp = "+temp); x = temp; System.out.println("x = "+x); 

a saída será,

 temp = 0 x = 0 

post increment significa incrementar o valor e retornar o valor antes do incremento . É por isso que o valor temp é 0 . Então, o que se temp = i e isso está em um loop (exceto para a primeira linha de código). assim como na pergunta !!!!

O operador de incremento é aplicado à mesma variável à qual você está atribuindo. Isso está pedindo problemas. Tenho certeza de que você pode ver o valor da sua variável x enquanto executa este programa … isso deve deixar claro porque o loop nunca termina.