Що не в тому, щоб використовувати == для порівняння плавців на Java?


177

Відповідно до цієї сторінки java.sun == є оператором порівняння рівності для чисел з плаваючою комою на Java.

Однак, коли я набираю цей код:

if(sectionID == currentSectionID)

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

Що не так у використанні ==для порівняння значень з плаваючою комою? Який правильний спосіб це зробити? 


29
Оскільки порівнювати плавці з == проблематично, нерозумно використовувати їх як ідентифікатори; імена у вашому прикладі коду підказують, що це ви робите; переважні довгі цілі числа (longs), а фактичний стандарт для ідентифікаторів.
Карл Манастер


4
Так, це був лише випадковий приклад, чи ти насправді використовуєш поплавці як ідентифікатори? Чи є причина?
За Вікландер

5
"для плаваючих полів використовуйте метод Float.compare; а для подвійних полів використовуйте Double.compare. Спеціальна обробка плавких та подвійних полів необхідна наявністю Float.NaN, -0,0f та аналогічних подвійних констант; див. документацію Float.equals для детальної інформації. " (Джошуа Блох: Ефективна Java)
lbalazscs

Відповіді:


211

правильний спосіб перевірити floats на "рівність":

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

де епсилон - це дуже невелике число, наприклад 0,00000001, залежно від бажаної точності.


26
Дивіться посилання у прийнятій відповіді ( cygnus-software.com/papers/comparingfloats/comparingfloats.htm ), чому фіксований епсілон не завжди є хорошою ідеєю. Зокрема, оскільки значення поплавців, що порівнюються, стають великими (або малими), епсилон більше не підходить. (Використання epsilon чудово, якщо ви знаєте, що ваші знаки з плаваючою здатністю відносно розумні.)
PT

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

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] біт вправо", це плаваюча точка. Фіксована точка приймає положення точки радіації, яке має бути добре фіксованим. Крім того, оскільки зміщення точки binamal (?) Завжди може бути зроблено, щоб залишити вас "1" в крайньому лівому положенні, ви знайдете деякі системи, які опускають провідну "1", присвячуючи таким чином звільнений простір (1 біт!) до розширення діапазону експонента.
JustJeff

Проблема не має нічого спільного з двійковим та десятковим поданням. З десятковою плаваючою комою у вас все ще є такі речі, як (1/3) * 3 == 0,999999999999999999999999999999.
dan04

2
@ dan04 так, оскільки 1/3 не має десяткового АБО бінарного подання, у нього є тризначне подання і таким чином перетворить правильно :). Перелічені я цифри (.1, .25 тощо) мають ідеальні десяткові подання, але не мають двійкового представлення - і люди звикли до тих, хто має "точні" подання. БХД впорався б із ними ідеально. Ось і різниця.
Білл К

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 [*]. Якщо ви просто встановите ідентифікатор float на x, нічого не робите з ним (тобто немає арифметики) і пізніше перевіряйте наявність y == x, вам все буде добре. Також немає нічого поганого в тому, щоб використовувати їх як ключі в HashMap. Що ви не можете зробити, це припустити рівності x == (x - y) + y, і т. Д. Це, як говорять, люди зазвичай використовують цілі типи як ідентифікатори, і ви можете помітити, що більшість людей тут відкладені цим кодом, тому з практичних причин краще дотримуватися конвенцій . Зауважте, що існує стільки різних doubleзначень, скільки довгих values, тому ви нічого не отримуєте, використовуючи double. Крім того, генерування "наступного наявного ідентифікатора" може бути складним з подвоєнням і вимагає певних знань арифметики з плаваючою комою. Не варто біди.

  2. З іншого боку, покладатися на числову рівність результатів двох математично еквівалентних обчислень ризиковано. Це пояснюється помилками округлення та втратою точності при перетворенні від десяткового до двійкового подання. Це було обговорено на смерть на SO.

[*] Коли я сказав "JVM, сумісний зі стандартами", я хотів виключити певні реалізації JVM з пошкодженнями мозку. Дивіться це .


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

Вищезазначене стосується Float, а не float.
Quant_dev

Про що йдеться Float? Якщо спробувати створити таблицю унікальних floatзначень і порівняти їх ==, жахливі правила порівняння IEEE-754 призведуть до того, що таблиця буде заповнена NaNзначеннями.
supercat

floatтип не має equalsметоду.
Quant_dev

Ах - я не мав на увазі equalsметод екземпляра, а скоріше статичний метод (я думаю, що в Floatкласі), який порівнює два значення типу float.
supercat

9

Ця проблема не характерна для Java. Використання == для порівняння двох поплавків / подвійних / будь-яких десяткових чисел може потенційно спричинити проблеми через спосіб їх зберігання. Одноточний поплавок (згідно стандарту 754 IEEE) має 32 біти, розподілені таким чином:

1 біт - Знак (0 = позитивний, 1 = негативний)
8 біт - Експонент (спеціальне (зміщення-127) подання x у 2 ^ x)
23 біт - Мантіса. Діючий номер, який зберігається.

Мантіса - це те, що викликає проблему. Це як би наукове позначення, лише число в базі 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)яке не є точним для плаваючих точок. метод епсилон є найкращим підходом, який в цій відповіді: 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.


Якщо щось є ==, то вони ідентичні до біта. Як вони не могли бути рівними ()? Можливо, у вас це є назад?
mkb

@Matt NaN особливий. Double.isNaN (подвійний х) на Java реально реалізований як {return x! = X; } ...
Quant_dev

1
З плавцями ==не означає, що числа "ідентичні біту" (те саме число може бути представлене різними бітовими візерунками, хоча лише одна з них є нормалізованою формою). Крім того, -0.0fі 0.0fпредставлені різними бітовими шаблонами (біт знака різний), але порівнюють як рівний ==(але не з equals). Ваше припущення, що ==є побітним порівнянням, взагалі є неправильним.
Павло Мінаєв


5

Ви можете використовувати Float.floatToIntBits ().

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

1
Ви на правильному шляху. floatToIntBits () - це правильний шлях, але було б простіше просто використовувати функцію Float, вбудовану в equals (). Дивіться тут: stackoverflow.com/a/3668105/2066079 . Ви можете бачити, що за замовчуванням рівний () використовується 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.


при зміні float на подвійний цей метод не працює як 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

Чи маєте ви справу з аутсорсинговим кодом, який би використовував floats для речей, названих sectionID та currentSectionID? Просто цікаво.

@Bill K: "Двійкове представлення поплавця - це щось дратує". Як так? Як би ви зробили це краще? Є певні числа, які неможливо представити в будь-якій базі належним чином, оскільки вони ніколи не закінчуються. Пі - хороший приклад. Ви можете лише наблизити його. Якщо у вас є краще рішення, зверніться до Intel.


1

Як згадується в інших відповідях, парні можуть мати невеликі відхилення. І ви можете написати власний метод порівняння їх, використовуючи "прийнятне" відхилення. Однак ...

Існує клас apache для порівняння парних пар: org.apache.commons.math3.util.Precision

Він містить кілька цікавих констант: SAFE_MINіEPSILON , які є максимально можливими відхиленнями простих арифметичних операцій.

Він також забезпечує необхідні методи порівняння, рівних чи круглих парних пар. (з використанням виразки або абсолютного відхилення)


1

У одному рядковому відповіді, я можу сказати, ви повинні використовувати:

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

Щоб ви дізналися більше про правильне використання пов’язаних операторів, я докладно розглядаю тут деякі випадки: Як правило, є три способи тестування рядків у Java. Ви можете використовувати ==, .equals () або Objects.equals ().

Чим вони відрізняються? == тести на контрольну якість у рядках, що означає з'ясування того, чи є два об'єкти однаковими. З іншого боку, .equals () перевіряє, чи мають логічно два струни однакове значення. Нарешті, тести об’єктів.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

Один із способів зменшити похибку округлення - використовувати подвійний, а не плаваючий. Це не призведе до усунення проблеми, але це зменшує кількість помилок у вашій програмі, а плаваючий майже ніколи не є найкращим вибором. ІМХО.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.