Почему i = i + i дает мне 0?

96

У меня простая программа:

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

Когда я запускаю эту программу, все , что я вижу 0на iв моем выходе. Я ожидал, что в первый раз у нас будет i = 1 + 1, а i = 2 + 2затем и i = 4 + 4т. Д.

Это связано с тем, что, как только мы пытаемся повторно объявить iслева, его значение сбрасывается до 0?

Если бы кто-нибудь мог указать мне более тонкие детали этого, это было бы здорово.

Измените значение intна, longи кажется, что числа печатаются должным образом. Я удивлен, насколько быстро он достигает максимального 32-битного значения!

DeaIss
источник

Ответы:

168

Проблема связана с целочисленным переполнением.

В 32-битной арифметике с двоичным дополнением:

iдействительно начинает иметь значения степени двойки, но затем начинается поведение переполнения, когда вы дойдете до 2 30 :

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

... в intарифметике, поскольку это по существу арифметический модуль 2 ^ 32.

Луи Вассерман
источник
28
Не могли бы вы немного расширить свой ответ?
DeaIss
17
@oOTesterOo Он начинает печатать 2, 4 и т. д., но очень быстро достигает максимального значения целого числа и "переходит" к отрицательным числам, как только он достигает нуля, он остается на нуле навсегда
Ричард Тингл
52
Этот ответ даже не является полным (он даже не упоминает, что значение не будет 0на первых нескольких итерациях, но скорость вывода скрывает этот факт от OP). Почему это принято?
Гонки легкости на орбите
16
Предположительно это было принято, потому что ОП сочло это полезным.
Joe
4
@LightnessRacesinOrbit Хотя он напрямую не решает проблемы, поставленные OP в своем вопросе, ответ дает достаточно информации, чтобы приличный программист мог понять, что происходит.
Кевин
334

Введение

Проблема в целочисленном переполнении. Если он переполняется, он возвращается к минимальному значению и продолжается оттуда. Если он становится недостаточным, он возвращается к максимальному значению и продолжается оттуда. На изображении ниже показан одометр. Я использую это для объяснения переполнения. Это механический переполнение, но все же хороший пример.

В одометре, max digit = 9превышение максимального означает 9 + 1, что переносится и дает 0; Однако нет более высокой цифры, которую можно было бы изменить на a 1, поэтому счетчик сбрасывается на zero. Вы уловили идею - сейчас на ум приходят «целочисленные переполнения».

введите описание изображения здесь введите описание изображения здесь

Самый большой десятичный литерал типа int - 2147483647 (2 31 -1). Все десятичные литералы от 0 до 2147483647 могут появляться везде, где может появиться литерал int, но литерал 2147483648 может появляться только как операнд унарного оператора отрицания -.

Если при целочисленном сложении происходит переполнение, то результатом являются младшие биты математической суммы, представленные в некотором достаточно большом формате с дополнением до двух. Если происходит переполнение, то знак результата не совпадает со знаком математической суммы двух значений операндов.

Таким образом, 2147483647 + 1переполняется и оборачивается -2147483648. Следовательно int i=2147483647 + 1, будет переполнено, что не равно 2147483648. Кроме того, вы говорите «он всегда печатает 0». Это не так, потому что http://ideone.com/WHrQIW . Ниже эти 8 чисел показывают точку, в которой он вращается и переполняется. Затем он начинает печатать нули. Кроме того, не удивляйтесь, насколько быстро он вычисляет, современные машины быстры.

268435456
536870912
1073741824
-2147483648
0
0
0
0

Почему целочисленное переполнение "оборачивается"

Исходный PDF

Али Гаджани
источник
17
Я добавил анимацию для «Pacman» в символических целях, но она также служит отличным наглядным примером того, как можно увидеть «целочисленные переполнения».
Али Гаджани
9
Это мой любимый ответ на этом сайте за все время.
Ли Уайт
2
Похоже, вы упустили, что это была последовательность удвоения, а не добавления.
Paŭlo Ebermann
2
Я думаю, что анимация pacman получила этот ответ больше голосов, чем принятый ответ. Получите еще один голос за меня - это одна из моих любимых игр!
Husman
3
Для тех, кто не
понял
46

Нет, он не печатает только нули.

Измените его на это, и вы увидите, что произойдет.

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

То, что происходит, называется переполнением.

Петр Петров
источник
61
Интересный способ написать цикл for :)
Бернхард
17
@Bernhard Вероятно, чтобы сохранить структуру программы OP.
Taemyr
4
@Taemyr Наверное, но тогда его можно было бы заменить trueна i<10000:)
Бернхард
7
Я просто хотел добавить несколько утверждений; без удаления / изменения каких-либо утверждений. Я удивлен, что он привлек такое внимание.
peter.petrov
18
Вы могли бы использовать скрытый оператор, в while(k --> 0)просторечии называемый «пока kидет к 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);
        }
    }

вывод:

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

when sum > Integer.MAX_INT then assign i = 0;
TrungTran05T3
источник
4
Гм, нет, это просто работает для этой конкретной последовательности, чтобы достичь нуля. Попробуйте начать с 3.
Paŭlo Ebermann
4

Поскольку у меня недостаточно репутации, я не могу опубликовать изображение вывода для той же программы на C с контролируемым выводом, вы можете попробовать себя и убедиться, что он действительно печатает 32 раза, а затем, как объяснялось, из-за переполнения i = 1073741824 + 1073741824 изменяется на -2147483648 и еще одно добавление выходит за пределы диапазона int и превращается в 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
источник
3
Эта программа на C фактически запускает неопределенное поведение при каждом выполнении, что позволяет компилятору заменять всю программу чем угодно (даже если system("deltree C:")вы находитесь в DOS / Windows). Знаковое целочисленное переполнение - это неопределенное поведение в C / C ++, в отличие от Java. Будьте очень осторожны при использовании такой конструкции.
filcab
@filcab: "заменить всю программу чем угодно", о чем ты. Я запустил эту программу в Visual Studio 2012, и она отлично работает для обоих signed and unsignedцелых чисел без какого-либо неопределенного поведения
Kaify
3
@Kaify: Нормальная работа - совершенно допустимое неопределенное поведение. Однако представьте, что код выполнялся i += iдля 32+ итераций, а затем имел if (i > 0). Компилятор может оптимизировать это, if(true)так как, если мы всегда добавляем положительные числа, iвсегда будет больше 0. Он также может оставить условие, где оно не будет выполняться, из-за переполнения, представленного здесь. Поскольку компилятор может создать из этого кода две равнозначные программы, его поведение не определено.
3Doubloons
1
@Kaify: это не лексический анализ, это компилятор, который компилирует ваш код и, следуя стандарту, может делать «странные» оптимизации. Как петля, о которой говорили 3D-дублоны. Просто потому, что компиляторы, которые вы пытаетесь, всегда что-то делают, это не означает стандартных гарантий, что ваша программа всегда будет работать одинаково. У вас было неопределенное поведение, часть кода могла быть удалена, потому что нет возможности добраться туда (UB гарантирует это). Эти сообщения из блога llvm (и ссылки там) содержат дополнительную информацию: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab
2
@Kaify: Извините, что не поставил это, но совершенно неправильно говорить «держал это в секрете», особенно когда это второй результат в Google для «неопределенного поведения», который был конкретным термином, который я использовал для того, что срабатывает. .
filcab
4

Значение iхранится в памяти с использованием фиксированного количества двоичных цифр. Когда для числа требуется больше цифр, чем доступно, сохраняются только самые низкие цифры (самые высокие цифры теряются).

Сложение iс собой - это то же самое, что умножение iна два. Точно так же, как умножение числа на десять в десятичной системе счисления можно выполнить, сдвигая каждую цифру влево и ставя ноль справа, умножение числа на два в двоичной системе счисления может выполняться таким же образом. Это добавляет одну цифру справа, поэтому цифра теряется слева.

Здесь начальное значение равно 1, поэтому, если мы используем 8 цифр для хранения i(например),

  • после 0 итераций значение равно 00000001
  • после 1 итерации значение равно 00000010
  • после 2 итераций значение равно 00000100

и так до последнего ненулевого шага

  • после 7 итераций значение 10000000
  • после 8 итераций значение равно 00000000

Независимо от того, сколько двоичных цифр выделено для хранения числа и независимо от начального значения, в конечном итоге все цифры будут потеряны, поскольку они смещены влево. После этого продолжение удвоения числа не изменит число - оно все равно будет представлено всеми нулями.

Звездный ребенок
источник
3

Это правильно, но после 31 итерации 1073741824 + 1073741824 вычисляет некорректно и после этого выводит только 0.

Вы можете выполнить рефакторинг для использования BigInteger, чтобы ваш бесконечный цикл работал правильно.

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);
        }
    }
}
Бруно Вольпато
источник
Если я использую long вместо int, кажется, что это долгое время печатает> 0 чисел. Почему после 63 итераций эта проблема не возникает?
DeaIss
1
«Не рассчитывает правильно» - неверная характеристика. Расчет верен в соответствии с тем, что должно произойти в спецификации Java. Реальная проблема в том, что результат (идеального) расчета не может быть представлен в виде int.
Stephen C
@oOTesterOo - потому что longможет представлять большие числа, чем intможет.
Stephen C
Long имеет больший диапазон. Тип BigInteger принимает любое значение / длину, которую может выделить ваша JVM.
Bruno Volpato
Я предположил, что int переполняется после 31 итерации, потому что это 32-битное максимальное число, и до тех пор, пока 64-битное число достигнет своего максимума после 63? Почему это не так?
DeaIss
2

Для отладки таких случаев полезно уменьшить количество итераций в цикле. Используйте это вместо своего while(true):

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

Затем вы можете видеть, что он начинается с 2 и удваивает значение, пока не вызовет переполнение.

user3732069
источник
2

Я буду использовать 8-битное число для иллюстрации, потому что его можно полностью подробно описать в коротком месте. Шестнадцатеричные числа начинаются с 0x, а двоичные - с 0b.

Максимальное значение для 8-битного целого числа без знака - 255 (0xFF или 0b11111111). Если вы добавите 1, вы обычно ожидаете получить: 256 (0x100 или 0b100000000). Но поскольку это слишком много бит (9), это превышает максимум, поэтому первая часть просто отбрасывается, оставляя вам фактически 0 (0x (1) 00 или 0b (1) 00000000, но с отброшенным 1).

Итак, когда ваша программа запускается, вы получаете:

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
...
богатый Ремер
источник
1

Самый большой десятичный литерал типа int- 2147483648 (= 2 31 ). Все десятичные литералы от 0 до 2147483647 могут появляться везде, где может появиться литерал int, но литерал 2147483648 может появляться только как операнд унарного оператора отрицания -.

Если целочисленное сложение переполняется, результатом являются младшие биты математической суммы, представленные в некотором достаточно большом формате с дополнением до двух. Если происходит переполнение, то знак результата не совпадает со знаком математической суммы двух значений операндов.

Scooba Doo
источник