Почему бесконечности с плавающей точкой, в отличие от NaN, равны?
Почему бесконечное сравнение не следует логике, примененной к NaNs? Этот код распечатывает false
три раза:
double a = Double.NaN;
double b = Double.NaN;
System.out.println(a == b); // false
System.out.println(a < b); // false
System.out.println(a > b); // false
Однако, если я изменю Double.NaN
в Double.POSITIVE_INFINITY
, Я получил true
для равенства, но false
для сравнений больше и меньше чем:
double a = Double.POSITIVE_INFINITY;
double b = Double.POSITIVE_INFINITY;
System.out.println(a == b); // true
System.out.println(a < b); // false
System.out.println(a > b); // false
Это кажется опасным. Предполагая, что бесконечные значения являются результатом переполнения, я полагаю, что более вероятно, что две переменные, которые оказались бесконечными, на самом деле не будут равны в идеальной арифметике.
8 ответов
Ваше мнение таково, что Double.POSITIVE_INFINITY
не должно быть равным самому себе, потому что оно "вероятно" было получено в результате потери точности.
Эта линия рассуждений относится ко всем числам с плавающей точкой. Любое конечное значение может быть получено в результате неточной операции. Это не подтолкнуло комитет по стандартизации IEEE 754 к определению ==
как всегда оценивается как ложное для конечных значений, так почему же бесконечности должны быть разными?
Как определено, ==
полезен для людей, которые понимают, что он делает (то есть проверяет полученные значения с плавающей запятой, и, конечно, не значения, которые должны были быть получены с помощью реальных вычислений). Для тех, кто понимает это, и вы должны понимать, чтобы использовать плавающую точку даже для вычислений, которые не включают бесконечность, имея Double.POSITIVE_INFINITY == Double.POSITIVE_INFINITY
Функция оценки в true удобна, если только для проверки, является ли результат вычисления с плавающей точкой вычислением с плавающей запятой Double.POSITIVE_INFINITY
,
Это оставляет вопрос о том, почему NaN может позволить себе иметь особое поведение, и бесконечности должны следовать тем же общим принципам, что и конечные значения. NaN отличается от бесконечности: основной принцип стандарта IEEE 754 состоит в том, что значения являются именно такими, какие они есть, но результат операции может быть аппроксимирован относительно реального результата, и в этом случае результирующее значение с плавающей запятой получается в соответствии с режимом округления.
Забудь на мгновение, что 1.0 / 0.0
определяется как +inf, что раздражает в этом обсуждении. Подумайте на минуту Double.POSITIVE_INFINITY
только в результате таких операций, как 1.0e100 / 1.0e-300
или же Double.MAX_VALUE + Double.MAX_VALUE
, Для этих операций +inf является ближайшим приближением к реальному результату, так же как и для операций, которые дают конечный результат. Напротив, NaN - это результат, который вы получаете, когда операция не имеет смысла. Целесообразно, чтобы NaN вел себя особенно, но inf - это всего лишь приближение всех значений, слишком больших для представления.
В действительности, 1.0 / 0.0
также производит +inf, но это следует считать исключением. Было бы столь же логично определить результат этой операции как NaN, но определить его как +inf было удобнее при реализации некоторых алгоритмов. Пример приведен на странице 10 в заметках Кахана. Более подробная информация, чем большинство пожелает, содержится в статье "Отрезки ветвей для сложных элементарных функций или много шума из ничего не значащего". Я также интерпретировал бы существование в IEEE 754 флага "деление на ноль", отдельного от флага NaN, как признание того, что пользователь может захотеть трактовать деление на ноль специально, хотя это не определено как создание NaN.
Потому что это стандарт. Бесконечность представляет число больше или меньше Double.MAX_VALUE / -Double.MAX_VALUE.
NaN представляет собой результат операции, которая не имеет смысла. То есть операция, возможно, не вышла с номером.
Я предполагаю, что логика заключается в том, что когда число становится достаточно большим (бесконечность) и из-за ограничения чисел с плавающей запятой, добавление к нему чисел не изменит результат, поэтому его "как" бесконечность.
Поэтому, если вы хотите сравнить с действительно большими числами, в какой-то момент вы можете просто сказать, что эти два больших числа достаточно близки для всех намерений и целей. Но если вы хотите сравнить две вещи, которые не являются числами, вы не можете сравнить их, так что это неверно. По крайней мере, вы не могли бы сравнить их как примитив.
Почему бесконечности равны? Потому что это работает.
Арифметика с плавающей точкой разработана для (относительно) быстрых вычислений, которые сохраняют ошибки. Идея состоит в том, что вы не проверяете переполнения или другие глупости во время длительного расчета; вы ждете, пока это не закончится. Вот почему NaN распространяют так, как они делают: как только вы получили NaN, вы можете сделать очень мало вещей, которые заставят его исчезнуть. Когда вычисления завершены, вы можете искать NaN, чтобы проверить, что-то пошло не так.
То же самое для бесконечностей: если есть возможность переполнения, не делайте вещей, которые отбрасывают бесконечности.
Если вы хотите работать медленно и безопасно, в IEEE-754 есть механизмы для установки обработчиков прерываний, которые обеспечивают обратные вызовы в коде, когда результатом вычисления будет NaN или бесконечность. В основном это не используется; как правило, он слишком медленный и бессмысленный, когда код правильно отлажен (не так уж просто: люди получают докторскую степень в том, как хорошо это делать).
Другая перспектива, оправдывающая равенство "бесконечных" ценностей, заключается в том, чтобы вообще избежать концепции кардинальности. По сути, если вы не можете рассуждать о том, "насколько бесконечное значение сравнивается с другим, учитывая, что оба бесконечно", проще предположить, что Inf = Inf
,
Редактировать: в качестве пояснения к моему комментарию относительно кардинальности я приведу два примера относительно сравнения (или равенства) бесконечных величин.
Рассмотрим множество натуральных чисел S1 = {1,2,3, ...}
, который бесконечен. Также рассмотрим множество четных целых S2 = {2,4,6, ...}
, которые тоже бесконечны. Хотя в S1 явно вдвое больше элементов, чем в S2, они имеют "одинаковое количество" элементов, поскольку между наборами можно легко выполнять взаимно-однозначную функцию, т.е. 1 -> 2
, 2-> 4
... Таким образом, они имеют одинаковую мощность.
Рассмотрим вместо этого набор действительных чисел R
и множество целых I
, Снова оба бесконечные множества. Однако для каждого целого числа i
Есть бесконечно много действительных чисел между (i, i+1)
, Таким образом, никакая функция "один к одному" не может отобразить элементы этих двух наборов, и, следовательно, их мощность различна.
Итог: равенство бесконечных величин сложно, его проще избежать в императивных языках:)
Мне кажется, что "потому что он должен вести себя так же, как ноль", было бы хорошим ответом. Арифметическое переполнение и переполнение должны обрабатываться аналогично.
Если вы опуститесь от наибольшего почти бесконечно малого значения, которое может быть сохранено в float, вы получите ноль, и нули будут сравниваться как идентичные.
Если вы переполняете самое большое почти бесконечно большое значение, которое может быть сохранено в float, вы получаете INF, и INF сравниваются как идентичные.
Это означает, что код, который обрабатывает числа, которые не входят в область действия в обоих направлениях, не потребует отдельного специального регистра для одного или другого. Вместо этого либо оба, либо ни один из них не должны рассматриваться по-разному.
И самое простое требование охватывается случаем "ни того, ни другого": вы хотите проверить, что что-то переполнено / переполнено, вы можете сравнить его с нулем /INF, используя только обычные арифметические операторы сравнения, без необходимости знать специальный синтаксис текущего языка для проверяющая команда: это Math.isInfinite(), Float.checkForPositiveInfinity(), hasOverflowed()...?
Поскольку было упомянуто Double.Nan.equals (Double.NaN): это то, что должно происходить, когда вы выполняете арифметику и сравнивать числа, это совершенно другое, когда вы рассматриваете, как должны вести себя объекты.
Два типичных проблемных случая: сортировка массива чисел и использование хеш-значений для реализации словарей, наборов и т. Д. Есть два исключительных случая, когда нормальное упорядочение с помощью <, = и> не применяется: один случай - это +0 = -0, а другой - то, что NaN ≠ NaN, и x
С этим могут столкнуться алгоритмы сортировки. Алгоритм сортировки может предполагать, что x = x всегда верно. Поэтому, если я знаю, что x хранится в массиве, и ищу его, я могу не проверять границы, потому что при его поиске нужно что-то найти. Нет, если х это NaN. Алгоритм сортировки может предполагать, что ровно одно из a = b должно быть истинным. Нет, если человек NaN. Таким образом, наивный алгоритм сортировки может дать сбой, когда присутствуют NaN. Вам нужно решить, где вы хотите, чтобы NaN заканчивались при сортировке массива, а затем изменить код сравнения, чтобы он работал.
Теперь словари и наборы и вообще хеширование: что если я использую NaN в качестве ключа? Набор содержит уникальные объекты. Если набор содержит NaN, и я пытаюсь добавить еще один, является ли он уникальным, потому что он не равен тому, который уже существует? А как насчет +0 и -0, их следует считать равными или разными? Существует правило, что любые два элемента, считающиеся равными, должны иметь одинаковое значение хеш-функции. Поэтому разумно (вероятно), что хеш-функция возвращает одно уникальное значение для всех NaN и одно уникальное значение для +0 и -0. И после поиска хеша, когда вам нужно найти элемент с таким же значением хеша, которое фактически равно, два NaN должны считаться равными (но отличными от всего остального).
Вероятно, поэтому Double.Nan.equal () ведет себя не так, как ==.
Правильный ответ прост: "потому что стандарт (и документы) так говорят". Но я не стану циничным, потому что очевидно, что это не то, что вам нужно.
В дополнение к другим ответам здесь я попытаюсь связать бесконечности с насыщающей арифметикой.
В других ответах уже указывалось, что причина сравнения NaN приводит к true
так что я не собираюсь бить мертвую лошадь.
Допустим, у меня есть насыщающее целое число, представляющее цвета в оттенках серого. Почему я использую насыщающую арифметику? Потому что все, что ярче белого, все еще белое, а все, что темнее черного, остается черным (кроме оранжевого). Это означает BLACK - x == BLACK
а также WHITE + x == WHITE
, Имеет смысл?
Теперь, скажем, мы хотим представить эти цвета в градациях серого с помощью 8-разрядного целого числа со знаком (со знаком) 1s, где BLACK == -127
а также WHITE == 127
, Почему 1с дополняют? Потому что это дает нам ноль со знаком, как IEEE 754 с плавающей точкой. И, потому что мы используем насыщающую арифметику, -127 - x == -127
а также 127 + x == 127
,
Как это связано с бесконечностью с плавающей точкой? Замените целое число с плавающей точкой, BLACK
с NEGATIVE_INFINITY
, а также WHITE
с POSITIVE_INFINITY
а что ты получаешь? NEGATIVE_INFINITY - x == NEGATIVE_INFINITY
а также POSITIVE_INFINITY + x == POSITIVE_INFINITY
,
Так как вы использовали POSITIVE_INFINITY
Я буду использовать это также. Сначала нам нужен класс для представления нашего насыщающего целочисленного цвета; давай называть это SaturatedColor
и предположим, что он работает как любое другое целое число в Java. Теперь давайте возьмем ваш код и заменим double
с нашим собственным SaturatedColor
а также Double.POSITIVE_INFINITY
с SaturatedColor.WHITE
:
SaturatedColor a = SaturatedColor.WHITE;
SaturatedColor b = SaturatedColor.WHITE;
Как мы установили выше, SaturatedColor.WHITE
(просто WHITE
выше) 127
Итак, давайте сделаем это здесь:
SaturatedColor a = 127;
SaturatedColor b = 127;
Теперь мы берем System.out.println
заявления вы использовали и заменить a
а также b
с их значением (значениями):
System.out.println(127 == 127);
System.out.println(127 < 127);
System.out.println(127 > 127);
Должно быть очевидно, что это будет печатать.
Это потому, что NaN не является числом и, следовательно, не равно любому числу, включая NaN.