CodeGym /Kursy /JAVA 25 SELF /Problemy z precyzją i wartości specjalne

Problemy z precyzją i wartości specjalne

JAVA 25 SELF
Poziom 6 , Lekcja 3
Dostępny

1. Wprowadzenie

Wydaje się, że skoro komputer „mądry”, to 0.1 + 0.2 powinno być po prostu 0.3. Ale nie do końca tak jest. Przyjrzyjmy się temu na prostym przykładzie.


double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // Co wypisze program?
Dodawanie liczb zmiennoprzecinkowych: oczekujemy 0.3, a co w rzeczywistości?

A teraz spróbuj porównać z 0.3:

System.out.println(sum == 0.3); // A tutaj będzie true czy false?

Jeśli zobaczysz false, nie dziw się!

Przyczyna tkwi w reprezentacji liczb w pamięci

Komputery operują na liczbach w systemie binarnym. Jednak nie każdą liczbę dziesiętną da się przedstawić jako skończoną liczbę binarną, tak jak 1/3 nie da się dokładnie zapisać liczbą dziesiętną (0.333...). Na przykład 0.1 w systemie binarnym — to ułamek nieskończony i trzeba go „zaokrąglać” do przechowywania.

Mówiąc potocznie, double czasem „udaje”, że przechowuje Twoją liczbę dokładnie, lecz w rzeczywistości przechowuje jedynie bardzo bliską wartość przybliżoną.

2. Jakie „dziwne efekty” mogą wystąpić w arytmetyce z double?

Przyjrzyjmy się praktycznym przykładom.

Przykład 1. Klasyczna „magia” 0.1 + 0.2

double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum);            // 0.30000000000000004
System.out.println(sum == 0.3);     // false

Komputer wypisał nie 0.3, lecz 0.30000000000000004. Różnica jest mała, ale jeśli zajmujesz się np. finansami — to już bywa krytyczne.

Przykład 2. Dodawanie iteracyjne

double result = 0;
for (int i = 0; i < 10; i++)
{
    result += 0.1;
}
System.out.println(result); // 0.9999999999999999

Chcieliśmy 1.0 — otrzymaliśmy odrobinę mniej. Znów powodem jest zaokrąglanie wewnątrz double.

Dlaczego to ważne w realnych zadaniach

Wielu myśli: „Co za różnica — drobny błąd, trudno!” Rozważmy przykład ze świata płatności.

Załóżmy, że Twój internetowy bank sumuje 100 transakcji po 0.1 euro. Jeśli Twój program „gubi” jedną stutysięczną euro przy każdej iteracji, to w skali banku już „stracisz” realne pieniądze. Wtedy natychmiast przyjdzie do Ciebie księgowy i zapyta: „Gdzie są nasze pieniądze?!”

3. Jak poprawnie porównywać liczby zmiennoprzecinkowe

Ponieważ double często nie może przechowywać dokładnie tej wartości, której oczekujesz, bezpośrednie porównanie operatorem == może zawodzić. Zamiast tego przyjęło się porównywać wartość bezwzględną różnicy z pewnym bardzo małym progiem (epsilon).

Przykład porównania z tolerancją

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;

if (Math.abs(a - b) < epsilon)
{
    System.out.println("Prawie równe!"); // Tak porównywać jest bezpieczniej
}

Tutaj mówimy: „Jeśli różnica między liczbami jest mniejsza niż jedna milionowa, uznajemy, że liczby są równe”.
Uwaga: funkcja Math.abs(value) zwraca wartość bezwzględną (moduł) przekazanej liczby.

4. Wartości specjalne typu double: Infinity, NaN, -Infinity

Typ double przechowuje nie tylko liczby, ale i wartości specjalne. Pojawiają się one w sytuacjach, których matematyk zwykłemu studentowi surowo zabrania.

Nieskończoność (Infinity)

Co się stanie, jeśli podzielimy 1 przez 0?

double result = 1.0 / 0.0;
System.out.println(result); // Infinity

W Javie (i wielu językach) dzielenie przez 0 dla double nie powoduje wyjątku! Zamiast tego wynik staje się specjalną wartością „dodatnia nieskończoność”.

Ujemna nieskończoność (-Infinity)

Jeśli podzielić liczbę ujemną przez 0, to otrzymasz ujemną nieskończoność:

double result = -1.0 / 0.0;
System.out.println(result); // -Infinity

„Nie liczba” (NaN — Not a Number)

Jeśli zrobić coś naprawdę osobliwego, na przykład spróbować obliczyć pierwiastek z liczby ujemnej:

double result = Math.sqrt(-1);
System.out.println(result); // NaN

Albo wynik dzielenia 0.0 / 0.0:

double result = 0.0 / 0.0;
System.out.println(result); // NaN

NaN — to „wszystko, co nie byłoby liczbą w prawdziwym życiu”.

Sprawdzanie wartości specjalnych

W Javie są funkcje do sprawdzania wartości specjalnych:

System.out.println(Double.isInfinite(result));    // true, jeśli nieskończoność
System.out.println(Double.isNaN(result));         // true, jeśli NaN

Tabela: Jak double reaguje na nietypowe operacje

Operacja Wynik Co jest przechowywane w double
1.0 / 0.0
Infinity +∞
-1.0 / 0.0
-Infinity -∞
0.0 / 0.0
NaN Nie liczba
Math.sqrt(-1)
NaN Nie liczba

5. Typowe błędy przy pracy z liczbami zmiennoprzecinkowymi

Błąd nr 1: Porównywanie liczb zmiennoprzecinkowych operatorem ==
Najczęstsza pułapka: próba sprawdzenia, czy dwie obliczone liczby zmiennoprzecinkowe są równe za pomocą zwykłego porównania. Z powodu kumulacji błędów zaokrągleń niemal zawsze otrzymasz nieoczekiwany wynik. Zawsze używaj porównania z tolerancją (epsilon).

Błąd nr 2: Nieoczekiwane NaN i Infinity w obliczeniach
Jeśli nie kontrolujesz dzielenia przez zero lub pierwiastków z liczb ujemnych, w programie mogą pojawić się NaN albo Infinity i „zarazić” wszystkie kolejne obliczenia. Nie zapominaj sprawdzać podejrzanych wartości za pomocą Double.isNaN() i Double.isInfinite() — to pomoże uniknąć niemiłych niespodzianek.

Błąd nr 3: Używanie NaN jako „markera”
Niektórzy początkujący używają NaN jako „specjalnej” wartości kontrolnej, np. „jeśli nie znaleziono — zwróćmy NaN”. Pamiętaj jednak: NaN jest podstępny i porównanie operatorem == nie zadziała! Sprawdzaj wyłącznie za pomocą metod specjalnych.

Błąd nr 4: Oczekiwanie wyjątku przy dzieleniu przez 0.0
W odróżnieniu od dzielenia liczb całkowitych, dzielenie przez 0.0 dla double nie powoduje błędu, lecz zwraca Infinity lub NaN. Może to prowadzić do cichych i trudnych do wychwycenia błędów — wynik jest, ale nie taki, jakiego oczekiwałeś.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION