Por que i = i + i me dá 0?

96

Tenho um programa simples:

public class Mathz {
    static int i = 1;
    public static void main(String[] args) {    
        while (true){
            i = i + i;
            System.out.println(i);
        }
    }
}

Quando executo este programa, tudo o que vejo é 0a iminha saída. Eu esperava que a primeira vez que tivéssemos sido i = 1 + 1, seguido por i = 2 + 2, seguido por i = 4 + 4etc.

Isso se deve ao fato de que, assim que tentamos declarar novamente ino lado esquerdo, seu valor é redefinido para 0?

Se alguém puder me apontar os detalhes mais sutis disso, seria ótimo.

Altere intpara longe parece estar imprimindo números conforme o esperado. Estou surpreso com a rapidez com que atinge o valor máximo de 32 bits!

DeaIss
fonte

Respostas:

168

O problema é devido ao estouro de inteiro.

Na aritmética de complemento de dois de 32 bits:

ide fato começa tendo valores de potência de dois, mas os comportamentos de estouro começam quando você chega a 2 30 :

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

... em intaritmética, já que é essencialmente aritmético mod 2 ^ 32.

Louis Wasserman
fonte
28
Você poderia expandir um pouco a sua resposta?
DeaIss
17
@oOTesterOo Começa a imprimir 2, 4 etc, mas muito rapidamente atinge o valor máximo do inteiro e "volta" para números negativos, uma vez que chega a zero fica em zero para sempre
Richard Tingle
52
Essa resposta nem mesmo está completa (nem mesmo menciona que o valor não estará 0nas primeiras iterações, mas a velocidade de saída está obscurecendo esse fato do OP). Por que isso é aceito?
Lightness Races in Orbit
16
Presumivelmente, foi aceito por ser considerado útil pelo OP.
Joe
4
@LightnessRacesinOrbit Embora não trate diretamente dos problemas que o OP colocou em sua pergunta, a resposta fornece informações suficientes para que um programador decente seja capaz de inferir o que está acontecendo.
Kevin
334

Introdução

O problema é o estouro de inteiro. Se transbordar, ele volta ao valor mínimo e continua a partir daí. Se for insuficiente, ele voltará ao valor máximo e continuará a partir daí. A imagem abaixo é de um odômetro. Eu uso isso para explicar transbordamentos. É um estouro mecânico, mas ainda é um bom exemplo.

Em um odômetro, o max digit = 9, portanto, indo além dos meios máximos 9 + 1, que transporta e dá um 0; No entanto, não há dígito superior para alterar para a 1, portanto, o contador é redefinido para zero. Você começa a idéia - "estouro de inteiros" vêm à mente agora.

insira a descrição da imagem aqui insira a descrição da imagem aqui

O maior literal decimal do tipo int é 2147483647 (2 31 -1). Todos os literais decimais de 0 a 2147483647 podem aparecer em qualquer lugar em que um literal int possa aparecer, mas o literal 2147483648 pode aparecer apenas como o operando do operador de negação unário -.

Se uma adição inteira estourar, o resultado serão os bits de ordem inferior da soma matemática representada em algum formato de complemento de dois suficientemente grande. Se ocorrer estouro, o sinal do resultado não é o mesmo que o sinal da soma matemática dos dois valores do operando.

Assim, 2147483647 + 1transborda e envolve -2147483648. Conseqüentemente, int i=2147483647 + 1seria transbordado, o que não é igual a 2147483648. Além disso, você diz "sempre imprime 0". Não é assim, porque http://ideone.com/WHrQIW . Abaixo, esses 8 números mostram o ponto em que ele gira e transborda. Em seguida, começa a imprimir 0s. Além disso, não se surpreenda com o quão rápido ele calcula, as máquinas de hoje são rápidas.

268435456
536870912
1073741824
-2147483648
0
0
0
0

Por que o estouro de inteiro "envolve"

PDF original

Ali Gajani
fonte
17
Eu adicionei a animação de "Pacman" para fins simbólicos, mas também serve como um ótimo visual de como alguém veria 'estouro de inteiros'.
Ali Gajani
9
Esta é a minha resposta favorita neste site de todos os tempos.
Lee White
2
Parece que você não percebeu que se tratava de uma sequência de duplicação, não de adição.
Paŭlo Ebermann
2
Acho que a animação do pacman teve essa resposta mais votos positivos do que a resposta aceita. Dê outro voto positivo em mim - é um dos meus jogos favoritos!
Husman
3
Para quem não entendeu o simbolismo: en.wikipedia.org/wiki/Kill_screen#Pac-Man
wei2912
46

Não, ele não imprime apenas zeros.

Mude para isto e você verá o que acontece.

    int k = 50;
    while (true){
        i = i + i;
        System.out.println(i);
        k--;
        if (k<0) break;
    }

O que acontece é chamado de estouro.

peter.petrov
fonte
61
Maneira interessante de escrever um loop for :)
Bernhard
17
@Bernhard Provavelmente é para manter a estrutura do programa do OP.
Taemyr
4
@Taemyr Provavelmente, mas ele poderia ter substituído truepor i<10000:)
Bernhard
7
Eu só queria adicionar algumas declarações; sem excluir / alterar quaisquer declarações. Estou surpreso que tenha atraído tanta atenção.
peter.petrov
18
Você poderia ter usado o operador oculto while(k --> 0)coloquialmente chamado de "enquanto kvai para 0";)
Laurent LA RIZZA
15
static int i = 1;
    public static void main(String[] args) throws InterruptedException {
        while (true){
            i = i + i;
            System.out.println(i);
            Thread.sleep(100);
        }
    }

resultado:

2
4
8
16
32
64
...
1073741824
-2147483648
0
0

when sum > Integer.MAX_INT then assign i = 0;
TrungTran05T3
fonte
4
Hum, não, funciona apenas para esta sequência particular chegar a zero. Tente começar com 3.
Paŭlo Ebermann
4

Como não tenho reputação suficiente, não posso postar a imagem da saída para o mesmo programa em C com saída controlada, você pode tentar você mesmo e ver que realmente imprime 32 vezes e, em seguida, conforme explicado devido ao estouro i = 1073741824 + 1073741824 muda para -2147483648 e mais uma adição está fora do intervalo de int e muda para Zero.

#include<stdio.h>
#include<conio.h>

int main()
{
static int i = 1;

    while (true){
        i = i + i;
      printf("\n%d",i);
      _getch();
    }
      return 0;
}
Kaify
fonte
3
Este programa, em C, na verdade dispara um comportamento indefinido em cada execução, o que permite ao compilador substituir todo o programa por qualquer coisa (até mesmo system("deltree C:"), já que você está no DOS / Windows). O estouro de inteiro assinado é um comportamento indefinido em C / C ++, ao contrário de Java. Tenha muito cuidado ao usar esse tipo de construção.
filcab
@filcab: "substitua todo o programa por qualquer coisa" do que você está falando. Eu signed and unsigned
executei
3
@Kaify: Trabalhar bem é um comportamento indefinido perfeitamente válido. Imagine, no entanto, que o código funcionou i += ipor mais de 32 iterações e depois o fez if (i > 0). O compilador poderia otimizar isso para if(true)já que se estamos sempre adicionando números positivos, isempre será maior que 0. Ele também pode deixar a condição em, onde não será executado, por causa do estouro representado aqui. Como o compilador pode produzir dois programas igualmente válidos a partir desse código, é um comportamento indefinido.
3Doubloons de
1
@Kaify: não é análise léxica, é o compilador compilando seu código e, seguindo o padrão, podendo fazer otimizações “esquisitas”. Como o loop de que o 3Doubloons falou. Só porque os compiladores que você tenta sempre parecem fazer algo, isso não significa que o padrão garante que seu programa sempre funcionará da mesma maneira. Você tinha um comportamento indefinido, algum código pode ter sido eliminado já que não há como chegar lá (UB garante isso). Estas postagens do blog llvm (e links nele) têm mais informações: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab
2
@Kaify: Desculpe por não ter colocado, mas é totalmente errado dizer “manteve em segredo”, principalmente quando é o segundo resultado, no Google, para “comportamento indefinido”, que foi o termo específico que usei para o que estava sendo acionado .
filcab de
4

O valor de ié armazenado na memória usando uma quantidade fixa de dígitos binários. Quando um número precisa de mais dígitos do que os disponíveis, apenas os dígitos mais baixos são armazenados (os dígitos mais altos são perdidos).

Somando ia si mesmo é o mesmo que multiplicar ipor dois. Assim como multiplicar um número por dez em notação decimal pode ser executado deslizando cada dígito para a esquerda e colocando um zero à direita, multiplicar um número por dois em notação binária pode ser executado da mesma maneira. Isso adiciona um dígito à direita, de modo que um dígito se perde à esquerda.

Aqui, o valor inicial é 1, então, se usarmos 8 dígitos para armazenar i(por exemplo),

  • após 0 iterações, o valor é 00000001
  • após 1 iteração, o valor é 00000010
  • após 2 iterações, o valor é 00000100

e assim por diante, até a etapa final diferente de zero

  • após 7 iterações, o valor é 10000000
  • após 8 iterações, o valor é 00000000

Não importa quantos dígitos binários são alocados para armazenar o número e não importa qual seja o valor inicial, eventualmente todos os dígitos serão perdidos conforme eles são empurrados para a esquerda. Depois desse ponto, continuar a dobrar o número não mudará o número - ele ainda será representado por zeros.

filho da estrela
fonte
3

Está correto, mas após 31 iterações, 1073741824 + 1073741824 não calcula corretamente e depois disso imprime apenas 0.

Você pode refatorar para usar BigInteger, para que seu loop infinito funcione corretamente.

public class Mathz {
    static BigInteger i = new BigInteger("1");

    public static void main(String[] args) {    

        while (true){
            i = i.add(i);
            System.out.println(i);
        }
    }
}
Bruno Volpato
fonte
Se eu usar long em vez de int, parecerá imprimir> 0 números por um longo tempo. Por que ele não encontra esse problema após 63 iterações?
DeaIss
1
"Não calcula corretamente" é uma caracterização incorreta. O cálculo está correto de acordo com o que a especificação Java diz que deve acontecer. O verdadeiro problema é que o resultado do cálculo (ideal) não pode ser representado como um int.
Stephen C
@oOTesterOo - porque longpode representar números maiores do que intpodem.
Stephen C
Longo tem um alcance maior. O tipo BigInteger aceita qualquer valor / comprimento que sua JVM possa alocar.
Bruno Volpato
Presumi que o int overflows após 31 iterações porque é um número de tamanho máximo de 32 bits, e assim que um 64 bits atingiria seu máximo após 63? Por que não é esse o caso?
DeaIss
2

Para depurar tais casos, é bom reduzir o número de iterações no loop. Use isto em vez de while(true):

for(int r = 0; r<100; r++)

Você pode ver que ele começa com 2 e está dobrando o valor até causar um estouro.

user3732069
fonte
2

Usarei um número de 8 bits para ilustração porque ele pode ser completamente detalhado em um curto espaço. Os números hexadecimais começam com 0x, enquanto os números binários começam com 0b.

O valor máximo para um inteiro sem sinal de 8 bits é 255 (0xFF ou 0b11111111). Se você adicionar 1, normalmente espera obter: 256 (0x100 ou 0b100000000). Mas como são muitos bits (9), isso está acima do máximo, então a primeira parte é descartada, deixando você com 0 efetivamente (0x (1) 00 ou 0b (1) 00000000, mas com o 1 descartado).

Então, quando seu programa é executado, você obtém:

1 = 0x01 = 0b1
2 = 0x02 = 0b10
4 = 0x04 = 0b100
8 = 0x08 = 0b1000
16 = 0x10 = 0b10000
32 = 0x20 = 0b100000
64 = 0x40 = 0b1000000
128 = 0x80 = 0b10000000
256 = 0x00 = 0b00000000 (wraps to 0)
0 + 0 = 0 = 0x00 = 0b00000000
0 + 0 = 0 = 0x00 = 0b00000000
0 + 0 = 0 = 0x00 = 0b00000000
...
rico lembrete
fonte
1

O maior literal decimal do tipo inté 2147483648 (= 2 31 ). Todos os literais decimais de 0 a 2147483647 podem aparecer em qualquer lugar em que um literal int possa aparecer, mas o literal 2147483648 pode aparecer apenas como o operando do operador de negação unário -.

Se uma adição inteira estourar, o resultado serão os bits de ordem inferior da soma matemática representada em algum formato de complemento de dois suficientemente grande. Se ocorrer estouro, o sinal do resultado não é o mesmo que o sinal da soma matemática dos dois valores do operando.

Scooba doo
fonte