Что не так с использованием == для сравнения чисел в Java?


177

Согласно этой странице java.sun == является оператором сравнения равенства для чисел с плавающей точкой в ​​Java.

Тем не менее, когда я набираю этот код:

if(sectionID == currentSectionID)

в мой редактор и запустив статический анализ, я получаю: «JAVA0078 значения с плавающей точкой по сравнению с ==»

Что плохого в использовании ==для сравнения значений с плавающей запятой? Как правильно это сделать? 


29
Поскольку сравнение чисел с == проблематично, неразумно использовать их в качестве идентификаторов; имена в вашем примере кода указывают на то, что вы делаете; длинные целые числа (longs) являются предпочтительными, и де-факто стандарт для идентификаторов.
Карл Манастер


4
Да, это был случайный пример, или вы действительно используете поплавки в качестве идентификаторов? Есть ли причина?
В Викландере

5
«для полей с плавающей точкой используйте метод Float.compare; для двойных полей используйте Double.compare. Специальная обработка полей с плавающей точкой и двойных полей необходима из-за наличия Float.NaN, -0.0f и аналогичных двойных констант; подробности смотрите в документации к Float.equals. " (Джошуа Блох: Эффективная Ява)
lbalazscs

Ответы:


211

правильный способ проверить поплавки на «равенство»:

if(Math.abs(sectionID - currentSectionID) < epsilon)

где эпсилон - очень небольшое число, например 0,00000001, в зависимости от желаемой точности.


26
Посмотрите ссылку в принятом ответе ( cygnus-software.com/papers/comparingfloats/comparingfloats.htm ), чтобы узнать, почему фиксированный эпсилон не всегда хорошая идея. В частности, поскольку значения в сравниваемых поплавках становятся большими (или маленькими), эпсилон больше не подходит. (Использование epsilon - это хорошо, если вы знаете, что все значения с плавающей точкой относительно разумны.)
PT

1
@PT Может ли он умножить эпсилон на одно число и изменить функцию, if(Math.abs(sectionID - currentSectionID) < epsilon*sectionIDчтобы решить эту проблему?
энтузиастик

3
Это может быть даже лучшим ответом на данный момент, но он все еще несовершенен. Где вы получаете эпсилон?
Майкл Пифел

1
@MichaelPiefel это уже говорит: «в зависимости от желаемой точности». Плавания по своей природе являются своего рода физическими значениями: вас интересует только ограниченное количество позиций в зависимости от общей погрешности, любые отличия между ними считаются спорными.
ivan_pozdeev

Но ОП действительно хотел только проверить на равенство, и, поскольку это, как известно, ненадежно, должен использовать другой метод. Тем не менее, я не понимаю, что он знает, какова его «желаемая точность»; так что если вам нужен более надежный тест на равенство, то остается вопрос: откуда вы берете эпсилон? Я предложил использовать Math.ulp()в своем ответе на этот вопрос.
Майкл Пифель

53

Значения с плавающей точкой могут быть немного отключены, поэтому они могут не отображаться как абсолютно равные. Например, если задать значение с плавающей запятой равным «6,1», а затем снова распечатать его, вы можете получить сообщаемое значение, например «6,099999904632568359375». Это очень важно для работы поплавков; следовательно, вы не хотите сравнивать их, используя равенство, а скорее сравнение в пределах диапазона, то есть, если разность числа с плавающей запятой и числа, с которым вы хотите сравнить, меньше определенного абсолютного значения.

Эта статья о реестре дает хороший обзор того, почему это так; полезное и интересное чтение.


@kevindtimm: так что вы будете делать свои тесты на равенство следующим образом, если (число == 6.099999904632568359375) любое время, когда вы хотите узнать число, равно 6,1 ... Да, вы правы ... все в компьютере строго детерминировано, только то, что аппроксимации, используемые для чисел с плавающей запятой, нелогичны при решении математических задач.
Ньютопия

Значения с плавающей точкой являются недетерминированно неточными на очень специфическом оборудовании .
Стюарт П. Бентли

1
@Stuart Я могу ошибаться, но я не думаю, что ошибка FDIV была недетерминированной. Ответы, предоставленные аппаратными средствами, не соответствовали спецификации, но они были детерминированными, так как одни и те же вычисления всегда давали один и тот же неверный результат
Гравитация

@Gravity Вы можете утверждать, что любое поведение является детерминированным, учитывая определенный набор предостережений.
Стюарт П. Бентли

Значения с плавающей точкой не являются неточными. Каждое значение с плавающей запятой это именно то, что оно есть. Что может быть неточным является результатом плавающей точкой расчета . Но будьте осторожны! Когда вы видите что-то вроде 0.1 в программе, это не значение с плавающей запятой. Это литерал с плавающей запятой - строка, которую компилятор преобразует в значение с плавающей запятой, выполняя вычисления .
Соломон Медленный

22

Просто чтобы объяснить причину того, что говорят все остальные.

Бинарное представление с плавающей точкой раздражает.

В двоичном коде большинство программистов знают соотношение между 1b = 1d, 10b = 2d, 100b = 4d, 1000b = 8d

Ну, это работает и по-другому.

.1b = .5d, .01b = .25d, .001b = .125, ...

Проблема заключается в том, что нет точного способа представления большинства десятичных чисел, таких как .1, .2, .3 и т. Д. Все, что вы можете сделать, это приблизительные в двоичном формате. Система делает небольшое округление, когда числа печатаются так, что она отображает .1 вместо .10000000000001 или .999999999999 (которые, вероятно, так же близки к сохраненному представлению, как и .1)

Редактировать из комментария: причина этого является нашими ожиданиями. Мы полностью ожидаем, что 2/3 будет помечено в какой-то момент, когда мы преобразуем его в десятичное число, или .7 или .67 или .666667 .. Но мы не ожидаем, что .1 будет округлено так же, как 2/3 - и это именно то, что происходит.

Кстати, если вам интересно, число, которое оно хранит внутри, является чисто двоичным представлением, использующим двоичную «научную нотацию». Таким образом, если вы скажете ему, чтобы хранить десятичное число 10,75d, он будет хранить 1010b для 10 и .11b для десятичного. Таким образом, он будет хранить .101011, а затем в конце сохранит несколько битов, чтобы сказать: переместите десятичную точку на четыре позиции вправо.

(Хотя технически это уже не десятичная точка, теперь это двоичная точка, но эта терминология не сделала бы вещи более понятными для большинства людей, которые нашли бы этот ответ для любого использования.)


1
@Matt K - не фиксированная точка; если вы «сохраните несколько битов в конце, скажем, переместите десятичную точку [N] битов вправо», это будет плавающая точка. Фиксированная точка принимает положение радикальной точки, ну, в общем, фиксированной. Кроме того, в общем, поскольку сдвиг бинамальной (?) Точки всегда может привести к тому, что вы оставите «1» в крайнем левом положении, вы обнаружите, что некоторые системы опускают начальную «1», выделяя таким образом освобожденное пространство (1). немного!) для расширения диапазона экспоненты.
JustJeff

Проблема не имеет ничего общего с двоичным и десятичным представлением. С десятичной плавающей запятой у вас все еще есть такие вещи, как (1/3) * 3 == 0.9999999999999999999999999999.
Ден04

2
@ dan04 да, потому что 1/3 не имеет десятичного ИЛИ двоичного представления, оно имеет триное представление и будет правильно конвертировать таким образом :). Числа, которые я перечислил (.1, .25 и т. Д.), Все имеют совершенные десятичные представления, но не имеют двоичного представления - и люди привыкли к тем, которые имеют «точные» представления. BCD справится с ними отлично. В этом разница.
Билл К

1
Это должно иметь гораздо больше голосов, поскольку описывает реальную проблему, стоящую за этой проблемой.
Левит

19

Что плохого в использовании == для сравнения значений с плавающей запятой?

Потому что это не правда, что 0.1 + 0.2 == 0.3


7
о чем Float.compare(0.1f+0.2f, 0.3f) == 0?
Водолей Сила

0,1f + 0,2f == 0,3f, но 0,1d + 0,2d! = 0,3d. По умолчанию 0,1 + 0,2 является двойным. 0,3 также удваивается.
burnabyRails

12

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

  1. Нет ничего плохого в использовании поплавков в качестве идентификаторов в JVM, совместимой со стандартами [*]. Если вы просто установите для плавающего идентификатора значение x, ничего с этим не сделаете (то есть без арифметики) и позже проверите для y == x, все будет хорошо. Также нет ничего плохого в использовании их в качестве ключей в HashMap. Что вы не можете сделать, так это предположить равенство и x == (x - y) + yт. Д. При этом люди обычно используют целочисленные типы в качестве идентификаторов, и вы можете заметить, что большинство людей здесь откладывают этот код, поэтому по практическим соображениям лучше придерживаться соглашений , Обратите внимание, что существует столько же различных doubleзначений, сколько и long values, поэтому вы ничего не получите, используя double. Кроме того, создание «следующего доступного идентификатора» может быть сложным с двойными числами и требует некоторых знаний арифметики с плавающей точкой. Не стоит хлопот.

  2. С другой стороны, полагаться на численное равенство результатов двух математически эквивалентных вычислений рискованно. Это связано с ошибками округления и потерей точности при преобразовании из десятичного в двоичное представление. Это было обсуждено до смерти на SO.

[*] Когда я сказал «стандартная JVM», я хотел исключить некоторые реализации JVM с поврежденным мозгом. Смотрите это .


При использовании чисел с плавающей точкой в ​​качестве идентификаторов необходимо соблюдать осторожность, чтобы гарантировать, что они сравниваются с использованием, ==а не equals, или гарантировать, что никакие числа с плавающей запятой, которые сравнивают неравные с самим собой, не будут храниться в таблице. В противном случае программа, которая пытается подсчитать, например, сколько уникальных результатов может быть получено из выражения при подаче различных входных данных, может рассматривать каждое значение NaN как уникальное.
суперкат

Вышесказанное относится к Float, а не к float.
quant_dev

О чем идет речь Float? Если попытаться построить таблицу уникальных floatзначений и сравнить их с ==ужасными правилами сравнения IEEE-754, таблица будет заполнена NaNзначениями.
суперкат

floatТип не имеет equalsметода.
quant_dev

Ах, я имел в виду не equalsметод экземпляра, а статический метод (я думаю, внутри Floatкласса), который сравнивает два значения типа float.
суперкат

9

Это проблема не только для Java. Использование == для сравнения двух чисел с плавающей запятой / двойных / любого десятичного числа может потенциально вызвать проблемы из-за способа их хранения. Число с плавающей запятой одинарной точности (согласно стандарту IEEE 754) имеет 32 бита, распределенных следующим образом:

1 бит - знак (0 = положительный, 1 = отрицательный)
8 бит - экспонента (специальное (смещение-127) представление x в 2 ^ x)
23 бита - Mantisa. Фактический номер, который сохраняется.

Богомол - то, что вызывает проблему. Это похоже на научную запись, только число в базе 2 (двоичное) выглядит как 1.110011 x 2 ^ 5 или что-то подобное. Но в двоичном коде первая 1 всегда является 1 (кроме представления 0)

Поэтому, чтобы сэкономить немного места в памяти (как каламбур), IEEE решил, что следует принять значение 1. Например, богомол 1011 действительно равен 1.1011.

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

У Java есть уникальная проблема в том, что язык универсален для множества различных платформ, каждая из которых может иметь свой собственный уникальный формат float. Это делает еще более важным избегать ==.

Правильный способ сравнить два числа с плавающей запятой (не относящихся к конкретному языку) на равенство заключается в следующем:

if(ABS(float1 - float2) < ACCEPTABLE_ERROR)
    //they are approximately equal

где ACCEPTABLE_ERROR равен #defined или некоторой другой константе, равной 0,000000001, или любой другой требуемой точности, как уже упоминал Виктор.

В некоторых языках есть эта функциональность или эта встроенная константа, но обычно это хорошая привычка.


3
Java имеет определенное поведение для поплавков. Это не зависит от платформы.
Ишай

9

На сегодняшний день быстрый и простой способ сделать это:

if (Float.compare(sectionID, currentSectionID) == 0) {...}

Однако в документах не указано четко значение разницы полей ( эпсилон из ответа @Victor), которое всегда присутствует в вычислениях с плавающей запятой, но это должно быть чем-то разумным, поскольку оно является частью стандартной библиотеки языков.

Тем не менее, если требуется более высокая или индивидуальная точность, то

float epsilon = Float.MIN_NORMAL;  
if(Math.abs(sectionID - currentSectionID) < epsilon){...}

это еще один вариант решения.


1
Документы, которые вы связали, указывают «значение 0, если f1 численно равно f2», что делает его таким же, как и (sectionId == currentSectionId)в случае с плавающей запятой. метод epsilon - лучший подход, который находится в этом ответе: stackoverflow.com/a/1088271/4212710
typoerrpr

8

Значения точки плавления не надежны из-за ошибки округления.

Как таковые они, вероятно, не должны использоваться в качестве ключевых значений, таких как sectionID. Вместо этого используйте целые числа или, longесли intне содержит достаточно возможных значений.


2
Согласовано. Учитывая, что это идентификаторы, нет причин усложнять арифметику с плавающей запятой.
Йонни

2
Или долго. В зависимости от того, сколько уникальных идентификаторов будет сгенерировано в будущем, int может быть недостаточно большим.
Уэйн Хартман

Насколько точен двойной по сравнению с поплавком?
Арвинд Мани

1
@ArvindhMani doubleгораздо точнее, но они также являются значениями с плавающей запятой, так что мой ответ должен был включать в себя floatи double.
Эрик Уилсон,

7

В дополнение к предыдущим ответам вы должны знать, что есть странное поведение, связанное с -0.0fи +0.0f(они есть, ==но нет equals) и Float.NaN(это есть, equalsно нет ==) (надеюсь, я правильно понял - ага, не делайте этого!).

Изменить: давайте проверим!

import static java.lang.Float.NaN;
public class Fl {
    public static void main(String[] args) {
        System.err.println(          -0.0f   ==              0.0f);   // true
        System.err.println(new Float(-0.0f).equals(new Float(0.0f))); // false
        System.err.println(            NaN   ==               NaN);   // false
        System.err.println(new Float(  NaN).equals(new Float( NaN))); // true
    }
} 

Добро пожаловать в IEEE / 754.


Если что-то ==, то они идентичны вплоть до бита. Как они могут не быть равными ()? Может быть, у вас есть это назад?
МКБ

@ Matt NaN особенный. Double.isNaN (double x) в Java фактически реализован как {return x! = X; } ...
quant_dev

1
С плавающей точкой ==это не означает, что числа «идентичны битам» (одно и то же число может быть представлено разными битовыми комбинациями, хотя только одна из них имеет нормализованную форму). Также, -0.0fи 0.0fпредставлены различными битовыми комбинациями (знак бит отличается), но сравниваются как равные ==(но не с equals). Ваше предположение о ==побитовом сравнении, вообще говоря, неверно.
Павел Минаев


5

Вы можете использовать Float.floatToIntBits ().

Float.floatToIntBits(sectionID) == Float.floatToIntBits(currentSectionID)

1
Ты на правильном пути. floatToIntBits () - правильный путь, но было бы проще использовать встроенную функцию equat () Float. Смотрите здесь: stackoverflow.com/a/3668105/2066079 . Вы можете видеть, что по умолчанию equals () использует floatToIntBits внутри.
dberm22

1
Да, если они являются объектами Float. Вы можете использовать приведенное выше уравнение для примитивов.
aamadmi

4

Прежде всего, они плавают или плавают? Если один из них является Float, вы должны использовать метод equals (). Также, вероятно, лучше всего использовать статический метод Float.compare.


4

Следующее автоматически использует лучшую точность:

/**
 * Compare to floats for (almost) equality. Will check whether they are
 * at most 5 ULP apart.
 */
public static boolean isFloatingEqual(float v1, float v2) {
    if (v1 == v2)
        return true;
    float absoluteDifference = Math.abs(v1 - v2);
    float maxUlp = Math.max(Math.ulp(v1), Math.ulp(v2));
    return absoluteDifference < 5 * maxUlp;
}

Конечно, вы можете выбрать более или менее 5 ULP («единица на последнем месте»).

Если вы в библиотеке Apache Commons, у Precisionкласса есть compareTo()и equals()с epsilon и с ULP.


при изменении числа с плавающей точкой на double этот метод не работает как isDoubleEqual (0,1 + 0,2-0,3, 0,0) == false
hychou

Кажется, вам нужно больше как 10_000_000_000_000_000L в качестве фактора, doubleчтобы покрыть это.
Майкл Пифель

3

Вы можете хотеть, чтобы это было ==, но 123.4444444444443! = 123.4444444444442



2

Два разных вычисления, которые дают одинаковые действительные числа, не обязательно дают одинаковые числа с плавающей запятой. Люди, которые используют == для сравнения результатов вычислений, обычно удивляются этому, поэтому предупреждение помогает пометить то, что в противном случае могло бы быть скрытой и трудной для воспроизведения ошибкой.


2

Имеете ли вы дело с внешним кодом, который будет использовать числа с плавающей запятой для вещей с именами sectionID и currentSectionID? Просто любопытно.

@ Билл К: «Бинарное представление с плавающей точкой раздражает». Как так? Как бы вы сделали это лучше? Есть определенные числа, которые не могут быть представлены в любой базе должным образом, потому что они никогда не заканчиваются. Пи хороший пример. Вы можете только приблизить это. Если у вас есть лучшее решение, обратитесь в Intel.


1

Как упоминалось в других ответах, двойники могут иметь небольшие отклонения. И вы можете написать свой собственный метод, чтобы сравнить их, используя «приемлемое» отклонение. Тем не мение ...

Существует класс apache для сравнения двойников: org.apache.commons.math3.util.Precision

Он содержит несколько интересных констант: SAFE_MINи EPSILON, которые являются максимально возможными отклонениями простых арифметических операций.

Он также предоставляет необходимые методы для сравнения, равенства или округления двойных чисел. (используя ulps или абсолютное отклонение)


1

В одной строке ответа я могу сказать, вы должны использовать:

Float.floatToIntBits(sectionID) == Float.floatToIntBits(currentSectionID)

Чтобы вы больше узнали о правильном использовании связанных операторов, я рассмотрю здесь несколько случаев: как правило, есть три способа тестирования строк в Java. Вы можете использовать ==, .equals () или Objects.equals ().

Насколько они разные? == проверяет эталонное качество в строках, что означает, что два объекта одинаковы. С другой стороны, .equals () проверяет, имеют ли две строки одинаковое значение логически. Наконец, Objects.equals () проверяет наличие любых нулей в двух строках, а затем определяет, вызывать ли .equals ().

Идеальный оператор для использования

Что ж, это было предметом многочисленных дискуссий, потому что каждый из трех операторов имеет свой уникальный набор сильных и слабых сторон. Например, == часто является предпочтительным вариантом при сравнении ссылки на объект, но в некоторых случаях может показаться, что он также сравнивает строковые значения.

Однако, то, что вы получаете, является падением, потому что Java создает иллюзию того, что вы сравниваете значения, но в действительности это не так. Рассмотрим два случая ниже:

Случай 1:

String a="Test";
String b="Test";
if(a==b) ===> true

Случай 2:

String nullString1 = null;
String nullString2 = null;
//evaluates to true
nullString1 == nullString2;
//throws an exception
nullString1.equals(nullString2);

Поэтому лучше использовать каждый оператор при тестировании определенного атрибута, для которого он предназначен. Но почти во всех случаях Objects.equals () является более универсальным оператором, поэтому опытные веб-разработчики выбирают его.

Здесь вы можете получить более подробную информацию: http://fluentthemes.com/use-compare-strings-java/


-2

Правильный путь будет

java.lang.Float.compare(float1, float2)

7
Float.compare (float1, float2) возвращает int, поэтому его нельзя использовать вместо float1 == float2 в условии if. Более того, это не решает основную проблему, на которую ссылается это предупреждение - что если числа с плавающей точкой являются результатами численного расчета, то float1! = Float2 может возникнуть только из-за ошибок округления.
Quant_dev

1
правильно, вы не можете копировать вставить, вы должны сначала проверить документ.
Эрик

2
Что вы можете сделать вместо float1 == float2 является Float.compare (float1, float2) == 0.
deterb

29
Это ничего вам не дает - вы все равно получаетеFloat.compare(1.1 + 2.2, 3.3) != 0
Павел Минаев

-2

Один из способов уменьшить ошибку округления - использовать double, а не float. Это не устранит проблему, но уменьшит количество ошибок в вашей программе, и float почти никогда не будет лучшим выбором. ПО МОЕМУ МНЕНИЮ.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.