Для какого значения i цикл while (i == i + 1) {} выполняется навсегда?

120

Я разгадал эту головоломку на продвинутом курсе программирования на экзамене в британском университете .

Рассмотрим следующий цикл, в котором я пока не объявлен:

while (i == i + 1) {}

Найдите определение i, которое предшествует этому циклу, чтобы цикл while продолжался вечно.

Следующий вопрос, который задавал тот же вопрос для этого фрагмента кода:

while (i != i) {}

было очевидно для меня. Конечно, в этой другой ситуации это так, NaNно я действительно застрял на предыдущей. Это связано с переполнением? Что заставило бы такой цикл навсегда зацикливаться в Java?

Джейк Маккензи
источник
3
Любые возможности переопределить .equals()метод? Поскольку i не объявлен, мы можем использовать любой класс, какой захотим.
Geno Chen
7
@Raedwald, изучая "непрофессиональный" код, делает вас более "профессиональным", так что ... В любом случае, это хороший вопрос
Андрей Тобилко
12
Интересный факт: в C # это также работает для числовых типов, допускающих значение NULL, значения которых равны null, поскольку null == nullистинно и null + 1равно null.
Эрик Липперт
4
@EricDuminil: Ситуация намного хуже, чем вы думаете. Во многих языках арифметика с плавающей запятой должна выполняться с точностью не менее 64 бита, указанной в double, что означает, что это может быть выполнено с более высокой точностью по желанию компилятора, и на практике это происходит . Я могу указать вам на дюжину вопросов на этом сайте от программистов C #, которые задаются вопросом, почему 0.2 + 0.1 == 0.3изменяется его значение в зависимости от настроек компилятора, фазы луны и так далее.
Эрик Липперт
5
@EricDuminil: Вина в этом беспорядке лежит на Intel, которая предоставила нам набор микросхем, который выполняет более точную и быструю арифметику с плавающей запятой, если числа могут быть зарегистрированы, что означает, что результаты вычислений с плавающей запятой могут изменять свои значения в зависимости от от того, насколько хорошо сегодня работает планировщик регистров в оптимизаторе. Тогда ваш выбор как разработчика языка - между повторяемыми вычислениями и быстрыми и точными вычислениями , и сообщество, которое заботится о математике с плавающей запятой, выберет последнее.
Эрик Липперт

Ответы:

142

Прежде всего, поскольку while (i == i + 1) {}цикл не меняет значение i, создание бесконечного цикла эквивалентно выбору значения, iкоторое удовлетворяет i == i + 1.

Таких значений много:

Начнем с «экзотических»:

double i = Double.POSITIVE_INFINITY;

или

double i =  Double.NEGATIVE_INFINITY;

Причина соответствия этим значениям i == i + 1указана в
JLS 15.18.2. Аддитивные операторы (+ и -) для числовых типов :

Сумма бесконечности и конечного значения равна бесконечному операнду.

Это не удивительно, поскольку добавление конечного значения к бесконечному должно привести к бесконечному значению.

Тем не менее, большинство значений, iкоторым удовлетворяют, i == i + 1являются просто большими double(или float) значениями:

Например:

double i = Double.MAX_VALUE;

или

double i = 1000000000000000000.0;

или

float i = 1000000000000000000.0f;

doubleИ floatтипы имеют ограниченную точность, поэтому если взять достаточно большое doubleили floatзначение, добавив 1к нему приведет к тому же значению.

Эран
источник
10
Или (double)(1L<<53)- илиfloat i = (float)(1<<24)
dave_thompson_085
3
@Ruslan: Любой математик не согласится. Числа с плавающей запятой не имеют смысла. Они неассоциативны, нерефлексивны (NaN! = NaN) и даже не могут быть заменены (-0 == 0, но 1/0! = 1 / -0). Так что большая часть алгебры неприменима.
Кевин
2
@Kevin, хотя числа с плавающей запятой действительно не могут иметь большого смысла в целом, поведение бесконечностей (которое описано в этом предложении) было разработано, чтобы иметь смысл.
Руслан
4
@Kevin Честно говоря, если вы имеете дело с бесконечностями или неопределенными значениями, вы также не можете использовать свойства, которые вы указали в алгебре.
Voo
2
@Kevin: ИМХО, математика с плавающей запятой могла бы иметь гораздо больше смысла, если бы они заменили понятия «положительный и отрицательный ноль», знаковые положительные, отрицательные и беззнаковые «бесконечно малые» вместе с одним «истинным нулем» и сделали NaN равно самому себе. Истинный ноль мог бы вести себя как аддитивное тождество во всех случаях, и что-то из операций, связанных с делением на бесконечно малые, потеряет склонность к предположению, что бесконечно малые числа положительны.
supercat
64

Эти головоломки подробно описаны в книге Джошуа Блоха и Нила Гафтера «Java Puzzlers: Traps, Pitfalls, and Corner Cases».

double i = Double.POSITIVE_INFINITY;
while (i == i + 1) {}

или:

double i = 1.0e40;
while (i == i + 1) {}

оба приведут к бесконечному циклу, потому что добавление 1к значению с плавающей запятой, которое достаточно велико, не изменит значение, потому что оно не «преодолевает разрыв» с его преемником 1 .

Замечание по поводу второй головоломки (для будущих читателей):

double i = Double.NaN;
while (i != i) {}

также приводит к бесконечному циклу, потому что NaN не равно никакому значению с плавающей запятой, включая само 2 .


1 - Головоломки Java: ловушки, подводные камни и угловые случаи (глава 4 - Петельные головоломки).

2 - JLS §15.21.1

Александр Пирогов
источник
1

двойной i = Double.POSITIVE_INFINITY;

Фаркас Джордж
источник
0

Просто идея: как насчет логических значений?

bool i = TRUE;

Разве это не случай где i + 1 == i?

Dominique
источник
зависит от языка. Многие языки автоматически переводят логические значения в целые числа в сочетании с целым числом. Другие делают то, что вы предлагаете, - приводят int к логическому.
Carl Witthoft
8
Это вопрос Java, и ваше предложение не проходит компиляцию на Java (в которой нет +оператора, который принимает операнды a booleanи an intas).
Eran
@Eran: в этом вся идея перегрузки оператора. Вы можете заставить логические значения Java вести себя как логические значения C ++.
Доминик
4
За исключением того, что Java не поддерживает перегрузку операторов, поэтому вы не можете.
CupawnTae