Por que isso entra em um loop infinito?


493

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 just x++ou or x=x+1, mas x = x++deve primeiro atribuir xa si mesmo e depois incrementá-lo. Por que xcontinuar com 0como 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."<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

}

Vou ler sobre as instruções para tentar entender ...


8
Eu suspeito que o que está acontecendo é: 1. carregar x em um registrador (= 0); 2. incremento x (x = 1); 3. salve o valor do registro em x (x = 0). No C / C ++, isso seria um comportamento indefinido, porque não há um ponto de sequência formal para definir a ordem de 2 e 3. Espero que alguém possa citar algo equivalente à especificação Java.
Rup

19
Tentamos isso em C ++ para ver o que aconteceria, e ele imprime 1,2,3 e sai. Eu não esperava aquilo. Presumo que seja dependente do compilador, pois é um comportamento indefinido. Usamos o gnu g ++.
grieve

13
@saj x++é pós-incremento; x=é atribuição de resultado ; o resultado de x++é o original x(e há um efeito colateral de incremento, mas isso não altera o resultado), portanto, isso pode ser interpretado como:var tmp = x; x++; x = tmp;
Marc Gravell

5
Agora que tenho uma pergunta popular, lamento, as respostas (repetidas) não param de aparecer, mesmo depois que a resposta correta foi escolhida. Minha tela "Atividade recente" está cheia das mesmas respostas, e está chegando ...
Tom Brito

3
@ Rob Vermeulen, você pode querer ler a pergunta completa antes de fazer um comentário ..;) Este foi um código criado por um aluno meu, e fiquei curioso por que desse comportamento.
Tom Brito

Respostas:


354

Nota : Originalmente, eu publiquei o código C # nesta resposta para fins de ilustração, pois o C # permite que você passe intparâmetros por referência com a refpalavra - chave. Decidi atualizá-lo com código Java legal real, usando a primeira MutableIntclasse que encontrei no Google para aproximar o que refé feito em c #. Não sei dizer se isso ajuda ou prejudica a resposta. Eu direi que pessoalmente não fiz tanto desenvolvimento em Java; então, pelo que sei, poderia haver muito mais maneiras idiomáticas para ilustrar esse ponto.


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

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

Direita? Incremente o valor passado e retorne o valor original: essa é a definição do operador pós-incremento.

Agora, vamos ver como esse comportamento se desenrola 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 estava antes do incremento . Esse valor de retorno é atribuído a x.

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

Isso pode ser mais claro ainda se reescrevermos o 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 xno lado esquerdo da atribuição acima por y", você pode ver que ela primeiro incrementa xe depois atribui a y" me parece confuso. Não é isso xque está sendo atribuído y; é o valor anteriormente atribuídox . Realmente, injetar ytorna 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.

Portanto, é claro: x = x++efetivamente não altera o valor de x. Sempre faz com que x tenha os valores x 0 , x 0 + 1 e x 0 novamente.


Atualização : Aliás, para que você não duvide que xseja atribuído a 1 "entre" a operação de incremento e a atribuição no exemplo acima, juntei uma demonstração rápida para ilustrar que esse valor intermediário realmente "existe", embora nunca seja "visto" no thread em execução.

A demo chama x = x++;em loop enquanto um thread separado imprime continuamente o valor de xpara 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.

Iniciando o encadeamento em segundo plano ...
0 0
0 0
1
1
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
1
0 0
1

1
Você não precisa criar uma classe para passar por referência em java (embora isso certamente funcione). Você pode usar a Integerclasse, que faz parte da biblioteca padrão, e ainda tem o benefício de receber uma caixa automática de e para int quase de forma transparente.
Rmeador 30/09/10

3
@rmeador Inteiro é imutável, então você ainda não pode alterar seu valor. AtomicInteger, no entanto, é mutável.
ILMTitan

5
@ Dan: By the way, xno seu último exemplo deve ser declarado volatile, caso contrário, é um comportamento indefinido e vendo 1s é implementação específica.
axtavt

4
@burkestar: Eu não acho que o link seja muito apropriado neste caso, já que é uma questão Java e (se não me engano) o comportamento é realmente indefinida em C ++.
Dan Tao

5
@ Tom Brito - em C não está definido ... o que ++ pode ser feito antes ou depois da atribuição. Na prática, pode haver um compilador que faça o mesmo que Java, mas você não gostaria de apostar nele.
detly 1/10/10

170

x = x++ funciona da seguinte maneira:

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

Portanto, a sequência de eventos se parece com a seguinte (é um bytecode descompilado real, produzido por 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 // Grava valor lembrado da pilha em x

Para comparação x = ++x:

   8: iinc 1, 1 // Incremento x
   11: iload_1 // Carrega o valor de x na pilha
   12: istore_1 // Salta o valor da pilha para x

se você fizer um teste, poderá ver que ele primeiro aumenta e depois atributos. Portanto, não deve atribuir zero.
Tom Brito

2
@ Tom esse é o ponto, no entanto - porque tudo isso é uma sequência única, ele está fazendo as coisas em uma ordem não óbvia (e provavelmente indefinida). Ao tentar testar isso, você adiciona um ponto de sequência e obtém um comportamento diferente.
Rup

Em relação à saída do bytecode: observe que iincincrementa uma variável, não incrementa o valor da pilha nem deixa um valor na pilha (diferente de quase todas as outras operações aritméticas). Você pode querer adicionar o código gerado por ++xpara comparação.
Anon

3
@ Rep Não pode ser definido em C ou C ++, mas em Java, está bem definido.
ILMTitan


104

Isso acontece porque o valor de xnão é incrementado.

x = x++;

é equivalente a

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

Explicação:

Vejamos o código de bytes para esta operação. Considere uma classe de amostra:

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

Agora, executando o desmontador de classe, 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."<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
}

Agora a Java VM é baseada em pilha, o que significa que, para cada operação, os dados serão enviados para a 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 variáveis ​​locais. As variáveis ​​locais recebem IDs, que são apenas os índices da matriz.

Vejamos as mnemônicas no main()método:

  • iconst_0: O valor constante 0 é enviado para a pilha.
  • istore_1: O elemento superior da pilha é exibido e armazenado na variável local com o índice 1
    que é x.
  • iload_1: O valor no local 1cujo valor x é 0, é empurrado para a pilha.
  • iinc 1, 1: O valor no local da memória 1é incrementado por 1. Então xagora se torna 1.
  • istore_1: O valor na parte superior da pilha é armazenado no local da memória 1. Isso é 0designado para x substituir seu valor incrementado.

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


5
Na verdade, é incrementado (é o significado de ++), mas a variável é substituída mais tarde.
Progman

10
int temp = x; x = x + 1; x = temp;é melhor não usar uma tautologia no seu exemplo.
Scott Chamberlain

52
  1. A notação de prefixo incrementará a variável ANTES da avaliação da expressão.
  2. A notação Postfix aumentará APÓS a avaliação da expressão.

No entanto " =" tem uma precedência de operador menor que "++ ".

Portanto, x=x++;deve avaliar da seguinte forma

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

Esta é a melhor resposta. Algumas marcações teriam ajudado a se destacar um pouco mais.
Justin Force

1
Isto está errado. Não se trata de precedência. ++tem precedência mais alta que =em C e C ++, mas a instrução é indefinida nesses idiomas.
Matthew Flaschen

2
A pergunta original é sobre Java
Jaydee

34

Nenhuma das respostas foi muito boa, então aqui vai:

Quando você está escrevendo int x = x++, não está atribuindo xa si próprio com o novo valor, mas sim xcom o valor de retorno da x++expressão. Que passa a ser 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. Mais tarde, porém, ao ler o valor de x, recebemos o valor atualizado, que é um deles.


Vou tentar compreender as linhas de bytecode, ver a minha atualização, por isso vai ser claro .. :)
Tom Brito

Usar println () foi muito útil para entender isso.
ErikE 30/09/10

29

Já foi bem explicado por outros. Apenas incluo os links para as seções relevantes de especificação Java.

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


1
+1. Essa é de longe a melhor resposta para a pergunta real: "Por quê?"
Matthew Flaschen

18

Esta afirmação:

x = x++;

avalia assim:

  1. Empurrar x para a pilha;
  2. Incremento x;
  3. Retire xda pilha.

Portanto, o valor é inalterado. Compare isso com:

x = ++x;

que avalia como:

  1. Incremento x;
  2. Empurrar x para a pilha;
  3. Retire xda pilha.

O que você quer é:

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

13
Definitivamente a implementação correta, mas a pergunta é 'por quê?'.
precisa saber é o seguinte

1
O código original estava usando pós-incremento em x e atribuindo-o a x. x será vinculado a x antes do incremento, portanto, nunca alterará valores.
Wkl

5
@cletus Eu não sou o derrotador, mas sua resposta inicial não continha a explicação. Apenas disse que 'x ++ `.
Petar Minchev 30/09/10

4
@cletus: Eu não diminuí a votação, mas sua resposta foi originalmente apenas o x++trecho de código.
precisa saber é o seguinte

10
A explicação também está incorreta. Se o código primeiro atribuísse x a x e depois incrementasse x, funcionaria bem. Basta alterar x++;sua solução x=x; x++;e você estará fazendo o que afirma que o código original está fazendo.
Wooble 30/09/10

10

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

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


1
Uau, há muitos detalhes nesta página quando a resposta é curta e simples, ou seja, esta.
Charles Goodwin

8

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

Os operadores de incremento / decremento podem ser aplicados antes (prefixo) ou depois (pós-fixado) do operando. O resultado do código ++; e resultado ++; ambos terminarão com o resultado sendo incrementado em 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 simples incremento / decremento, não importa realmente qual versão você escolhe. Mas se você usar esse operador em parte de uma expressão maior, a 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);

Que imprimirá 1 e 0.


1
Não é o resultado da avaliação que é o problema, é a ordem das lojas.
Rup

2
Discordo. Se x = 0, em seguida, x ++ retornará 0. Portanto x = x ++ resultará em x = 0.
Colin Cochrane

Rup está certo sobre isso. É a ordem das lojas que está em questão neste caso específico. y = x ++ não é o mesmo que x = x ++; No último, x está sendo atribuído a 2 valores na mesma expressão. Está sendo atribuído à mão esquerda x o resultado da avaliação da expressão x ++, que é 0. O lado direito x está sendo incrementado para 1. Na ordem em que essas duas atribuições ocorrem, trata-se do problema. Nas postagens anteriores, fica claro que isso funciona da seguinte maneira: eval = x ++ => eval == 0: incremento à direita x => x == 1: esquerda x = eval => x == 0
Michael Ekoka

7

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. atribua o resultado do lado direito (que foi salvo como 0) a x (x agora é 0)

A ideia é que o operador pós-incremento (x ++) aumente essa variável em questão DEPOIS de retornar seu valor para uso na equação em que é usado.

Editar: Adicionando um pouco por causa do comentário. Considere da seguinte maneira.

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.

OK, mas o que especifica a ordem das etapas 2 e 3?
Rup

@Rup - o idioma define. O lado direito da equação é avaliado primeiro (neste caso, "x ++") e o resultado é atribuído à variável no lado esquerdo. É assim que a linguagem funciona. Quanto ao "x ++" "retornando" x para a equação, é assim que o operador de incremento postfix funciona (retorne o valor de x e depois aumente). Se tivesse sido "--x", teria sido (incremento x, em seguida, retorne o valor). Retorno não é a palavra certa, mas você entendeu.
precisa saber é o seguinte

7

Você realmente não precisa 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 nessa 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.


5

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

x = 0;

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


se você fizer um teste, poderá ver que ele primeiro aumenta e depois atributos. Portanto, não deve atribuir zero.
Tom Brito

1
@ Tom: veja minha resposta - mostro em um teste que x ++ realmente retorna o valor antigo de x. É aí que parte.
Robert Munteanu

"se você fizer um teste" - algumas pessoas parecem pensar que um teste escrito em C nos diz o que o Java fará, quando nem sequer nos diz o que o C fará.
Jim Balter

3

O valor permanece em 0 porque o valor x++é 0. Nesse caso, não importa se o valor de xé aumentado ou não, a atribuição x=0é executada. Isso substituirá o valor incrementado temporário de x(que foi 1 por um "tempo muito curto").


Mas x ++ é uma operação pós. Portanto, x teria que ser incrementado após a conclusão da atribuição.
precisa

1
@Sagar V: apenas para a expressão x++, não por toda a atribuiçãox=x++;
Progman

Não, acho que só precisa ser incrementado após a leitura do valor de x a ser usado na atribuição.
Rup

1

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

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

1

Pense em x ++ como uma chamada de função que "retorna" o que X era antes do incremento (é por isso que é chamado de pós-incremento).

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


OK, mas o que especifica a ordem das etapas 3 e 4?
Rup

"retorna o que X era antes do incremento" está errado, veja minha atualização
Tom Brito

Na realidade, as etapas 3 e 4 não são operações separadas - não é realmente uma chamada de função que retorna um valor, apenas ajuda a pensar dessa maneira. Sempre que você tem uma atribuição, o lado direito é "avaliado" e o resultado é atribuído ao lado esquerdo, o resultado da avaliação pode ser considerado um valor de retorno, pois ajuda a entender a ordem das operações, mas não é realmente .
jhabbott

Opa, verdade. Eu quis dizer as etapas 2 e 4 - por que o valor retornado é armazenado por cima do valor incrementado?
Rup

1
Isso faz parte da definição de uma operação de atribuição, primeiro o lado direito é completamente avaliado e, em seguida, o resultado é atribuído ao lado esquerdo.
precisa saber é o seguinte

1

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


1

Bem, até onde eu posso ver, o erro ocorre devido à atribuição que substitui o valor incrementado, pelo valor anterior à incrementação, 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 incrementação.

Se você estiver interessado em investigar o bytecode, examinaremos as três linhas em questão:

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

7: iload_1 # Colocará o valor da 2ª variável local na pilha
8: iinc 1,1 # aumentará a 2ª variável local com 1, observe que ela deixa a pilha intocada!
9: istore_1 # Irá aparecer no 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 fará um loop indefinidamente, enquanto a versão com ++ x não. O bytecode para ++ x deve parecer bem diferente, tanto quanto me lembro do compilador Java 1.3 que escrevi há pouco mais de um ano, o bytecode deve ser algo como isto:

iinc 1,1
iload_1
istore_1

Então, apenas trocando as duas primeiras linhas, altera a semântica para que o valor deixado no topo da pilha, após o incremento (ou seja, o 'valor' da expressão) seja o valor após o incremento.


1
    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

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


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

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


0

Gostaria de saber se há algo na especificação Java que defina precisamente o comportamento disso. (A implicação óbvia dessa afirmação é que estou com preguiça de verificar.)

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

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

Se ele disse (pseudocódigo para pular detalhes técnicos)

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 ao escrever código que depende desse comportamento. Parece-me muito dependente da implementação, entre as rachaduras nas especificações. A única vez que faria diferença é se você fizesse algo louco, como o exemplo aqui, ou se você tivesse dois threads em execução e dependesse da ordem de avaliação na expressão.



0

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

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

0

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


0

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 .


1
Seu x/ yexemplo é diferente do código real e a diferença é relevante. Seu link nem menciona Java. Para duas das línguas que não menciona, a declaração em questão é indefinido.
Matthew Flaschen

0

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 incrementsignifica incrementar o valor e retornar o valor antes do incremento . É por isso que o valor tempé 0. Então, o que se temp = ie isso está em um loop (exceto para a primeira linha de código). assim como na pergunta !!!!


-1

O operador de incremento é aplicado à mesma variável que você está atribuindo. Isso está pedindo problemas. Estou certo de que você pode ver o valor da sua variável x durante a execução deste programa .... isso deve deixar claro por que o loop nunca termina.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.