JavaRush /Blog Java /Random-PL /Urządzenie liczb rzeczywistych

Urządzenie liczb rzeczywistych

Opublikowano w grupie Random-PL
Cześć! Na dzisiejszym wykładzie będziemy rozmawiać o liczbach w Javie, a konkretnie o liczbach rzeczywistych. Urządzenie liczb rzeczywistych - 1Nie panikować! :) Na wykładzie nie będzie żadnych trudności matematycznych. O liczbach rzeczywistych będziemy rozmawiać wyłącznie z naszego „programisty” punktu widzenia. Czym zatem są „liczby rzeczywiste”? Liczby rzeczywiste to liczby, które mają część ułamkową (która może wynosić zero). Mogą być pozytywne lub negatywne. Oto kilka przykładów: 15 56,22 0,0 1242342343445246 -232336,11 Jak działa liczba rzeczywista? Całkiem proste: składa się z części całkowitej, części ułamkowej i znaku. W przypadku liczb dodatnich znak zwykle nie jest podawany wprost, ale w przypadku liczb ujemnych jest on wskazany. Wcześniej szczegółowo sprawdziliśmy , jakie operacje na liczbach można wykonać w Javie. Wśród nich znalazło się wiele standardowych operacji matematycznych – dodawanie, odejmowanie itp. Było też dla Was kilka nowości: np. reszta z dzielenia. Ale jak dokładnie wygląda praca z liczbami w komputerze? W jakiej formie są przechowywane w pamięci?

Zapisywanie liczb rzeczywistych w pamięci

Myślę, że nie będzie to dla Was odkryciem, że liczby mogą być duże i małe :) Można je ze sobą porównywać. Przykładowo liczba 100 jest mniejsza od liczby 423324. Czy ma to wpływ na działanie komputera i naszego programu? Aktualnie tak . Każda liczba jest reprezentowana w Javie przez określony zakres wartości :
Typ Rozmiar pamięci (bity) Zakres wartości
byte 8 bitowy -128 do 127
short 16-bitowy -32768 do 32767
char 16-bitowy liczba całkowita bez znaku reprezentująca znak UTF-16 (litery i cyfry)
int 32 bity od -2147483648 do 2147483647
long 64 bity od -9223372036854775808 do 9223372036854775807
float 32 bity od 2 -149 do (2-2 -23 )*2 127
double 64 bity od 2 -1074 do (2-2 -52 )*2 1023
Dzisiaj porozmawiamy o dwóch ostatnich typach - floati double. Oba wykonują to samo zadanie - reprezentują liczby ułamkowe. Bardzo często nazywane są także „ liczbami zmiennoprzecinkowymi” . Zapamiętaj to określenie na przyszłość :) Na przykład liczba 2.3333 lub 134.1212121212. Trochę dziwne. W końcu okazuje się, że nie ma różnicy między tymi dwoma typami, skoro wykonują to samo zadanie? Ale jest różnica. Zwróć uwagę na kolumnę „rozmiar w pamięci” w powyższej tabeli. Wszystkie liczby (i nie tylko liczby - w ogóle wszystkie informacje) są przechowywane w pamięci komputera w postaci bitów. Bit to najmniejsza jednostka informacji. To całkiem proste. Dowolny bit jest równy 0 lub 1. A samo słowo „ bit ” pochodzi od angielskiego „ cyfry binarnej ” – liczby binarnej. Myślę, że prawdopodobnie słyszałeś o istnieniu systemu liczb binarnych w matematyce. Dowolną liczbę dziesiętną, którą znamy, można przedstawić jako zbiór zer i jedynek. Na przykład liczba 584,32 w formacie binarnym wyglądałaby tak: 100100100001010001111 . Każda jedynka i zero w tej liczbie to osobny bit. Teraz powinieneś jaśniej określić różnicę między typami danych. Na przykład, jeśli utworzymy liczbę typu float, mamy do dyspozycji tylko 32 bity. Podczas tworzenia liczby floatdokładnie tyle miejsca zostanie na nią przydzielone w pamięci komputera. Jeśli chcemy utworzyć liczbę 123456789.65656565656565, binarnie będzie ona wyglądać następująco: 11101011011110011010001010110101000000 . Składa się z 38 zer i jedynek, czyli do zapisania go w pamięci potrzeba 38 bitów. floatTa liczba po prostu nie „pasuje” do typu ! Dlatego liczbę 123456789 można przedstawić jako typ double. Do jego przechowywania przydzielono aż 64 bity: nam to odpowiada! Oczywiście zakres wartości również będzie odpowiedni. Dla wygody możesz myśleć o liczbie jak o małym pudełku z komórkami. Jeśli komórek jest wystarczająco dużo, aby zapisać każdy bit, typ danych jest wybierany poprawnie :) Urządzenie liczb rzeczywistych - 2Oczywiście na samą liczbę wpływa także różna ilość przydzielonej pamięci. Należy pamiętać, że typy floatmają doubleróżne zakresy wartości. Co to oznacza w praktyce? Liczba doublemoże wyrażać większą precyzję niż liczba float. 32-bitowe liczby zmiennoprzecinkowe (w Javie jest to dokładnie typ float) mają precyzję około 24 bitów, czyli około 7 miejsc po przecinku. A liczby 64-bitowe (w Javie jest to typ double) mają precyzję około 53 bitów, czyli około 16 miejsc po przecinku. Oto przykład, który dobrze pokazuje tę różnicę:
public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
Co w efekcie powinniśmy tutaj otrzymać? Wydawać by się mogło, że wszystko jest dość proste. Mamy liczbę 0,0 i dodajemy do niej 0,1111111111111111 7 razy z rzędu. Wynik powinien wynosić 0,7777777777777777. Ale stworzyliśmy numer float. Jego rozmiar jest ograniczony do 32 bitów i, jak powiedzieliśmy wcześniej, jest w stanie wyświetlić liczbę z dokładnością do około 7 miejsca po przecinku. Dlatego ostatecznie wynik, jaki otrzymamy w konsoli, będzie inny od tego, czego oczekiwaliśmy:

0.7777778
Numer wydawał się „odcięty”. Wiesz już, jak dane są przechowywane w pamięci - w postaci bitów, więc nie powinno Cię to dziwić. Jasne jest, dlaczego tak się stało: wynik 0,777777777777777 po prostu nie zmieścił się w przydzielonych nam 32 bitach, więc został obcięty, aby zmieścił się w zmiennej typu float:) Możemy zmienić typ zmiennej na doublew naszym przykładzie, a następnie końcowy wynik nie zostanie obcięty:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
Miejsc po przecinku jest już 16, wynik „mieści się” w 64 bitach. Swoją drogą, być może zauważyłeś, że w obu przypadkach wyniki nie były do ​​końca prawidłowe? Obliczenia wykonano z drobnymi błędami. Porozmawiamy o przyczynach tego poniżej :) Powiedzmy teraz kilka słów o tym, jak można porównywać liczby ze sobą.

Porównanie liczb rzeczywistych

Częściowo poruszyliśmy już tę kwestię na ostatnim wykładzie, kiedy mówiliśmy o operacjach porównawczych. Nie będziemy ponownie analizować operacji takich jak >, <, >=. <=Zamiast tego spójrzmy na bardziej interesujący przykład:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
Jak myślisz, jaka liczba wyświetli się na ekranie? Logiczną odpowiedzią byłaby odpowiedź: liczba 1. Liczenie zaczynamy od liczby 0,0 i sukcesywnie dodajemy do niej 0,1 dziesięć razy z rzędu. Wszystko wydaje się być w porządku, powinno być jedno. Spróbuj uruchomić ten kod, a odpowiedź bardzo Cię zaskoczy :) Dane wyjściowe konsoli:

0.9999999999999999
Ale dlaczego w tak prostym przykładzie wystąpił błąd? O_o Tutaj nawet piątoklasista z łatwością mógłby odpowiedzieć poprawnie, ale program Java dał niedokładny wynik. „Niedokładne” jest tutaj lepszym słowem niż „niepoprawne”. Nadal mamy liczbę bardzo bliską jedności, a nie jakąś losową wartość :) Różni się ona od prawidłowej dosłownie o milimetr. Ale dlaczego? Być może jest to tylko jednorazowy błąd. Może komputer się zawiesił? Spróbujmy napisać inny przykład.
public class Main {

   public static void main(String[] args)  {

       //dodaj 0,1 do zera jedenaście razy z rzędu
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Pomnóż 0,1 przez 11
       double f2 = 0.1 * 11;

       //powinien być taki sam - 1.1 w obu przypadkach
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Sprawdźmy!
       if (f1 == f2)
           System.out.println(„f1 i f2 są równe!”);
       else
           System.out.println(„f1 i f2 nie są równe!”);
   }
}
Wyjście konsoli:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Zatem na pewno nie jest to kwestia usterek komputera :) Co się dzieje? Tego typu błędy są związane ze sposobem przedstawiania liczb w postaci binarnej w pamięci komputera. Faktem jest, że w systemie binarnym nie można dokładnie przedstawić liczby 0,1 . Swoją drogą, system dziesiętny też ma podobny problem: nie da się poprawnie przedstawić ułamków zwykłych (i zamiast ⅓ dostajemy 0,33333333333333..., co też nie jest do końca poprawnym wynikiem). Wydawałoby się to drobnostką: przy takich obliczeniach różnica może wynosić sto tysięcznych części (0,00001) lub nawet mniej. Ale co, jeśli cały wynik Twojego Bardzo Poważnego Programu zależy od tego porównania?
if (f1 == f2)
   System.out.println(„Rakieta leci w kosmos”);
else
   System.out.println(„Start odwołany, wszyscy do domów”);
Wyraźnie spodziewaliśmy się, że te dwie liczby będą równe, ale ze względu na konstrukcję pamięci wewnętrznej odwołaliśmy start rakiety. Urządzenie liczb rzeczywistych - 3Jeśli tak, musimy zdecydować, jak porównać dwie liczby zmiennoprzecinkowe, aby wynik porównania był bardziej… hmmm… przewidywalny. Zatem poznaliśmy już zasadę nr 1 przy porównywaniu liczb rzeczywistych: nigdy nie używaj ==liczb zmiennoprzecinkowych przy porównywaniu liczb rzeczywistych. Ok, myślę, że wystarczy złych przykładów :) Spójrzmy na dobry przykład!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //dodaj 0,1 do zera jedenaście razy z rzędu
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Pomnóż 0,1 przez 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println(„f1 i f2 są równe”);
       else
           System.out.println(„f1 i f2 nie są równe”);
   }
}
Tutaj zasadniczo robimy to samo, ale zmieniamy sposób porównywania liczb. Mamy specjalną liczbę „progową” - 0,0001, jedna dziesięciotysięczna. Może być inaczej. Zależy to od tego, jak dokładne porównanie jest potrzebne w konkretnym przypadku. Możesz sprawić, że będzie większy lub mniejszy. Stosując tę ​​metodę, Math.abs()otrzymujemy moduł liczby. Moduł jest wartością liczby bez względu na znak. Przykładowo liczby -5 i 5 będą miały ten sam moduł i będą równe 5. Drugą liczbę odejmujemy od pierwszej i jeśli otrzymany wynik niezależnie od znaku będzie mniejszy od ustalonego przez nas progu, to nasze liczby są równe. W każdym razie są one równe stopniowi dokładności, jaki ustaliliśmy za pomocą naszej „liczby progowej”, czyli co najmniej równej co do jednej dziesięciotysięcznej. Ta metoda porównania uratuje Cię przed nieoczekiwanym zachowaniem, które zaobserwowaliśmy w przypadku ==. Innym dobrym sposobem porównywania liczb rzeczywistych jest użycie specjalnej klasy BigDecimal. Klasa ta została stworzona specjalnie do przechowywania bardzo dużych liczb z częścią ułamkową. W przeciwieństwie do doublei float, w przypadku BigDecimaldodawania, odejmowanie i inne operacje matematyczne są wykonywane nie przy użyciu operatorów ( +-itp.), ale przy użyciu metod. Tak to będzie wyglądać w naszym przypadku:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Utwórz dwa obiekty BigDecimal - zero i 0.1.
       Robimy to samo co poprzednio - dodajemy 0.1 do zera 11 razy pod rząd
       W klasie BigDecimal dodawanie odbywa się za pomocą metody add() */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Tu też nic się nie zmieniło: utwórz dwa obiekty BigDecimal
       i pomnóż 0,1 przez 11
       W klasie BigDecimal mnożenie odbywa się za pomocą metody multiple()*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Inną cechą BigDecimal jest to, że obiekty numeryczne muszą być porównywane ze sobą
       za pomocą specjalnej metody CompareTo()*/
       if (f1.compareTo(f2) == 0)
           System.out.println(„f1 i f2 są równe”);
       else
           System.out.println(„f1 i f2 nie są równe”);
   }
}
Jakie wyjście konsolowe otrzymamy?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Otrzymaliśmy dokładnie taki efekt, jakiego oczekiwaliśmy. I zwróć uwagę, jak dokładne okazały się nasze liczby i ile miejsc po przecinku mieści się w nich! Znacznie więcej niż w floati nawet w double! Zapamiętaj te zajęcia BigDecimalna przyszłość, na pewno będą Ci potrzebne :) Uff! Wykład był dość długi, ale dałeś radę: brawo! :) Do zobaczenia na kolejnej lekcji, przyszły programiście!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION