Operacje na liczbach zmiennoprzecinkowych

Spis treści

Tym razem zagadka może wydawać się dosyć prosta (i powinna taka być dla ludzi, którzy znają reprezentację liczb w komputerze). Pytanie dotyczy oczywiście jak zwykle tego co zobaczymy na ekranie po wykonaniu programu. Wykonuje on proste odejmowanie dwóch liczb zmiennoprzecinkowych typu double. Na pierwszy rzut oka odpowiedź wydaje się oczywista, ale czy na pewno tak jest?

public class JavaTraps004 {
	public static void main(String args[]) {
		double x = 2.0;
		double y = 1.1;
		System.out.println(x-y);
	}
}

Kurs Java

Odpowiedzi:

A) 0.9

B) -0.9

C) 1

D) 0.8999999999999999

Odpowiedź

Przypomnijmy kod, którego dotyczy zadanie:

public class JavaTraps004 {
	public static void main(String args[]) {
		double x = 2.0;
		double y = 1.1;
		System.out.println(x-y);
	}
}

Wydaje się proste i oczywiste, a oczekiwaną odpowiedzią jest 0.9, jednak o dziwo na ekranie pojawia się odpowiedź D) 0.8999999999999999. Ktoś kto nie zagłębiał się nigdy w konwersję liczb z systemu dziesiętnego na dwójkowy - do obojętnie jakiej formy, może być zaskoczony, dla pozostałych powinno być to dosyć zrozumiałe.

Liczby zmiennoprzecinkowe są reprezentowane w skrócie w taki sposób(nie zagłębiając się w kwestię kodowania):

2^m^+...+2^2^+2^1^+2^0^.(2^-1^)+(2^-2^)+(2^-3^)+...+(2^-n^)

Przyglądając się bliżej części ułamkowej widać, że nie zawsze da się uzyskać dokładną oczekiwaną wartość (jak np 0.5, czy 0.25), w takim wypadku otrzymujemy jedynie aproksymację (przybliżenie) danej wartości.

W takim wypadku oczywiste jest, że np w przypadku takiego działania:

double x = 2.0; 
double y = 1.5; 
System.out.println(x-y);

Otrzymamy oczekiwaną i prawidłową wartość 0.5, czyli 2^-1^

Jak sobie poradzić z tym problemem?

Oczywiście można sprytnie zamienić metodę print/println na printf z odpowiednio przygotowanym wzorcem:

double x = 2.0;
double y = 1.1;
System.out.printf("%.2f%n", x - y);

Otrzymamy w takim wypadku wartość 0.9, jednak nadal nie zmienia się tutaj kwestia reprezentacji liczby - nadal jest to przybliżenie wartości double.

Prawidłowym rozwiązaniem (szczególnie we wszelkich systemach obliczających dokładne wartości, gdy operujemy na pieniądzach) jest używanie klasy BigDecimal. To co należy dodatkowo zapamiętać, to używanie w przypadku typów BigDecimal konstruktorów przyjmujących typ String, otrzymujemy wtedy dokładnie taką wartość o jaką nam chodzi.

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.

Poniżej znajdziesz archiwalne wpisy z czasów, gdy strona była jeszcze hobbystycznym blogiem.

Leszek

Witam. Używam języka Fortran do obliczeń. Ale uczę się JAVY - chciałbym się na nią "przesiąść" a raczej korzystać z jej dobrodziejstwa ewentualnie połączyć obydwa programy. Ale pytanie jest taki - wykonałem w Fortranie (wiadomo język używany głównie do obliczeń) i ten przykład i wynik jest 0.9 - o co tu chodzi czy JAVA "gorzej liczy" albo czy Fortran przewidział "powyższe" i zawiera mechanizmy, które podają "dobry" wynik .

Leszek

Przepraszam - wynik jest 0.8999998 - stało się tak gdy kazałem wyświetlić " więcej miejsc po przecinku. Wcześniej po prostu z automatu Fortran zaokrąglił wynik - czyli zrobił to co na końcu w tym przykładzie - pozdrawiam Leszek

Czarownica

Strona jest kapitalna :) Mam prośbę: Mógłbyś dodać szerszy opis możliwości metody printf?

Czarownica

Oraz sposób czytania API?

Qbisiek

Chyba czegoś nie rozumiem, dlaczego raz piszesz, że nie da się zapisać wartości 0.5, binarnie, a raz że się da? Ponadto podajesz, że 0.5 = 2^-2, przecież 0.5 = 1/2 = 2^-1, natomiast 2^-2 = 1/4 = 0.25 - kolejna wartość którą trzeba niby aproksymować.

Qbisiek

PS. Ogólnie super stronka :)

Mirek

Wydaje mi się że zdanie zostało niefortunnie sformułowane i chodziło Autorowi o to że nie zawsze da się uzyskać oczekiwaną wartość taką jak 0,5 (w domyśle którą da się otrzymać). Natomiast rzeczywiście 0,5 to nie jest 2^-2 ale 2^-1 i tu już jest błąd w artykule.