Liczby zmiennoprzecinkowe

Do zapisywania liczb ułamkowych w języku Java możemy zastosować dwa typy zmiennych:

  • float – zmienna 32-bitowa typu Single Precision
  • double – zmienna 64-bitowa typu Double Precision

Zasadniczą różnicą między nimi jest większy zakres typu double, kosztem użycia 8 bajtów pamięci zamiast 4, jak w przypadku typu float. Ale w jaki sposób te liczby są zapisywane?

W szkole na etapie gimnazjum/szkoły średniej każdy spotkał się z notacją naukową liczb, która znacznie zwiększa klarowność zapisu liczb bardzo małych oraz bardzo dużych. Przykładowo prędkość światła wynosi w przybliżeniu 299 000 000 [m/s], jednak możemy ją zapisać również jako 2,99*108 [m/s] godząc się na utratę ostatnich cyfr znaczących. Już jest lepiej, ale ciągle nie wygląda to spektakularnie. Weźmy pod lupę masę spoczynkową neutronu, która wynosi:

0,000000000000000000000000001674 [kg]

Prościej jest ją zapisać jako 1,647*10-27 lub 1647*10-30 [kg]. W obydwu przypadkach sposób zapisu liczby bazuje na tej samej zasadzie. Najpierw określamy cyfry znaczące, czyli nasze 1647. Następnie określamy wartość wykładnika, czyli -30 wiedząc, że podstawą naszego systemu liczbowego jest liczba 10.

+1*1647*10-30

± 1 * mantysa * podstawa wykładnik

Bazując na tej zasadzie kodowane są liczby zmiennoprzecinkowe w systemie binarnym, wg obecnie przyjętego standardu IEEE 754. 64-bitowa zmienna typu Double Precision jest podzielona w następujący sposób:

  • 0 do 51:     53 bity mantysy, w których pierwszy bit znaczący jest zawsze równy 1 i nie jest jawnie kodowany. Z tego powodu nawet jeśli wszystkie jawne 52 bity są wyzerowane, to wartość mantysy wynosi 1,0000….0000 zamiast 0000…..0000.
  • 52 do 62:  11 bitów wykładnika pozwalające zakodować liczbę w zakresie -1023 do 1025. Binarnie 11 bitów pozwala zakodować liczbę w zakresie 0 do 2048, jednak w celu umożliwienia uzyskania również ujemnych wykładników od wartości zdekodowanej do postaci dziesiętnej odejmuje się stałą równą 1023.
  • 63:              bit znaku, ustawiony w przypadku wartości ujemnej, wygaszony w przypadku dodatniej

Zasadniczą różnicą w kodowaniu wg IEEE 754 w porównaniu z notacją naukową jest zastosowanie dodatkowej stałej do uzyskiwania ujemnego wykładnika zamiast użycia do tego celu jednego z bitów.

1 bit znaku * 1,bity mantysy * 2 wykładnik – 1023

Czas na przykład! Jak zapiszemy w tym formacie liczbę jeden?

  • mantysa: wszystkie 52 bity będą wyzerowane, zatem jej wartość to 1,0000….0000
  • wykładnik: całkowity wykładnik musi wynosić zero. Pamiętając, że od wykładnika odejmujemy stałą 1023 nasz zakodowany wykładnik musi mieć dokładnie taka samą wartość czyli binarnie 011 1111 1111.
  • bit znaku: wygaszony, ponieważ nasza wartość jest dodatnia

1 0 * 1,0000 (jeszcze 44 zera) 0000* 2 1023 (011 1111 1111 binarnie) – 1023

1 * 1,0 * 1

Mamy dokładnie zapisaną liczbę jeden. Ale czy możemy w ten sposób zapisać dokładnie każdy ułamek dziesiętny? Obierzmy za cel liczbę 0,3. Aby ją zapisać w/w sposobem należy przedstawić ją w formie działania:

1 0 * 1,2* 2 -2

Zakodowanie wykładnika oraz bitu znaku nie jest problemem. Problemem jest przedstawienie dokładnej wartości mantysy. Spróbujmy, włączając jej kolejne bity do gry, dojść do wartości 1,2. Dla przejrzystości pomijam wszystkie najmniej znaczące bity o wartości zero oraz bit niejawny kodujący wartość 1,0.

001 – 0,125

0011 – 0,125 + 0,0625 = 0,1875

0011001 – 0,125 + 0,0625 + 0,0078125 = 0,1953125

00110011 – 0,125 +0,0625 + 0,00078125 + 0,003900625 = 0,19921875

Zbliżamy się coraz bardziej do liczby 0,2 jednak nie jesteśmy w stanie jej osiągnąć za pomocą mantysy o skończonej długości ponieważ liczba 0,2 zapisana binarnie jest liczbą okresową (0011). Zatem nawet użycie zmiennej o długości np 256 bitów jedynie zwiększy precyzję nigdy nie osiągając wartości rzeczywistej. Korzystając z konwertera liczb na stronie IEEE 754 Converter można zobaczyć jak wygląda kodowanie innych liczb dziesiętnych oraz jaka jest jego dokładność.

Brak możliwości odzwierciedlenia rzeczywistej wartości niektórych liczb rzeczywistych za pomocą liczb zmiennoprzecinkowych niesie za sobą poważne konsekwencje w sytuacji kiedy podczas obliczeń zależy nam na szczególnie wysokiej dokładności jak np.

  • przeliczanie wartości pieniężnych
  • obliczenia naukowe wymagające wysokiej precyzji

W takich przypadkach w języku Java należy stosować typ BigDecimal, który zapewnia pożądaną dokładność.