Od zera do aplikacji developera – Liczby [#02]

simplecoding.pl 7 lat temu

Cześć, dzisiaj przejdziemy do trochę bardziej zaawansowanych operacji niż wypisywanie jednego komunikatu na ekran. Poznasz podstawowe struktury danych jakie są używane w Javie do przechowywania liczb, jak są one przechowywane oraz podstawowe operacje jakie możesz na nich wykonywać. Dostaniesz też pierwszą “pracę domową”.

Liczby stałoprzecinkowe

Liczby stałoprzecinkowe… jak to może brzmieć dla Ciebie skomplikowanie, a nie powinno. To są po prostu liczby, które mają zarezerwowaną ilość bitów na części dziesiętne liczby. Świetnym przykładem mogą być liczby całkowite, tam ilość bitów zarezerwowana na części ułamkowe jest stała, czyli 0. Liczby całkowite to mogą to być liczby stosunkowo małe, jak na przykład 1, 5 czy 10 oraz stosunkowo duże, na przykład 10940898, 32898423 lub jaką tam jeszcze sobie wymyślisz inną. Dlaczego o tym wspominam? Dla Ciebie każda taka liczba to po prostu… liczba. Dla komputera niekoniecznie. Komputer przechowuje liczby w postaci binarnej. Oznacza to, iż komputer każdą liczbę przechowuje jako kombinacje zer i jedynek. Nie jest to kombinacja losowa oczywiście. Nie chcę tu przynudzać i zaproszę Cię do filmiku Mirosława Zelenta, który tutaj świetnie opisuje systemy liczbowe, w tym binarne.

Ważne jest też do zapamiętania, iż w zapisie binarnym jedno zero lub jedna jedynka to jeden bit. Natomiast osiem bitów to jeden bajt. o ile obejrzałeś powyższy filmik i wiesz jak konwertować liczby to dla przykładu:

  • liczba 32760 to w zapisie binarnym 0111111111111000. Ma ona 16 znaków, a więc zajmuje 16 bitów. 16 bitów = 2 bajty
  • liczba 120 to w zapisie binarnym 01111000. Ma ona 8 znaków, więc zajmuje 8 bitów. 8 bitów = 1 bajt

Dobrze, już wiesz, czym jest system binarny, jak konwertować liczby oraz jak policzyć ile bitów/bajtów zajmuje.

Tylko po co masz to wiedzieć?

Zanim zapiszesz liczbę do komputera, komputer zwykle rezerwuje sobie określoną ilość miejsca w pamięci. Określana jest ona w bajtach. o ile wiemy, iż będziemy mieć liczby z zakresu od 1 do 100, gdzie 100, nasza największa liczba zajmuje 1 bajt (100 w binarnym to 01100100) to czy jest sens rezerwować na liczbę 8 bajtów? Oczywiście, iż nie. Byłoby to marnowanie pamięci (której w tej chwili w komputerach nie brakuje co prawda, ale warto optymalizować jeżeli można). Musimy jednak uważać na różne ewentualności. Może też się zdarzyć tak, iż zarezerwujemy 1 bajt, a będziemy mieli liczby zdecydowanie większe i będziemy mieli tzw. overflow czyli przepełnienie. Mirosław Zelent ten temat również świetnie omawia tutaj.

Przejdźmy teraz do adekwatnych typów w Javie. Dla liczb stałoprzecinkowych mamy cztery typy, które rozróżniamy w zależności od tego, ile miejsca one zajmują:

  • byte – 1 bajt
  • short – 2 bajty
  • int – 4 bajty
  • long – 8 bajtów

Jeżeli nie wiesz przez cały czas jaki zakres liczbowy obejmuje dany typ to wzór na to jest bardzo prosty. Dla danego typu zakres jest od -(2n-1) do 2n-1 – 1, gdzie n to ilość bitów zajmowana przez dany typ, czyli 8 * ilość bajtów. Od ilości bitów musimy odjąć 1, ponieważ jeden bit jest zarezerwowany dla znaku + lub -. Daje nam to zakresy liczbowe:

  • byte – od -128 do 127
  • short – od -32768 do 32767
  • int – od -2147483648 do 2147483647
  • long – od -2^(63) do (2^63) – 1(naprawdę duże liczby…)

Warto zaznaczyć, iż najczęściej spotykany jest typ int. Zależy to oczywiście od wymagań, które program musi spełnić. Warty odnotowania jest też fakt, iż liczby typu long zakończone są literką L (mała lub duża).

Liczby zmiennoprzecinkowe

Tutaj sprawy mają się podobnie jak w przypadku liczb stałoprzecinkowych. Różnica jest taka, iż liczby zmiennoprzecinkowe to nie muszą być liczby całkowite, czyli mogą to być liczby, które mają części ułamkowe. Dla liczb zmiennoprzecinkowych mamy dwa typy, które różnią się ilością zajmowanej pamięci oraz precyzją, czyli ilością liczb po przecinku. Typy te to:

  • float – 4 bajty, 6-7 liczb po przecinku
  • double – 8 bajtów, ~15 liczb po przecinku

Warto też zaznaczyć, iż do liczb dodajemy odpowiedni przyrostek, tak jak w przypadku liczb typu long. Dla float jest to f lub F, a dla double d lub D.

Liczby zmiennoprzecinkowe nie są jednak takie proste jakby się wydawało. Części ułamkowe dla komputera to suma ułamków ½ (czyli 2 do potęgi -1) podniesiona do odpowiedniej potęgi w zależności od tego, który bajt to jest. Problem jest taki, iż nie jesteśmy w stanie zapisać wszystkich liczb ułamkowych w postaci binarnej. Pierwszym z brzegu przykładem jest liczba 0,1. Może to prowadzić do wielu nieprzewidywalnych zachowań, dlatego gdy robimy ważne obliczenia (np. finansowe) i musimy operować na częściach dziesiętnych to warto zastosować specjalne klasy przystosowane do tego typu obliczeń, na przykład BigDecimal.

Ostatnim wartym odnotowania faktem jest to, iż część całkowitą od zmiennoprzecinkowej rozdzielamy kropką, nie przecinkiem!

Operacje na liczbach

Dobrze, ale po co nam liczby, skoro nie moglibyśmy nic z nimi zrobić? Słusznie myślisz! Tutaj dużej różnicy nie ma o ile chodzi o świat programowania a świat Javy. Operacje są zapisywane w postaci infiksowej, kolejność wykonywania działań jest taka, jak ogólnie przyjęta w prawdziwym świecie. Możemy używać nawiasów w obliczeniach. Operacje oraz odpowiadające im znaki to:

  • + dodawanie
  • odejmowanie
  • * mnożenie
  • / dzielenie
  • % modulo

Myślę, iż pierwszych czterech nie muszę omawiać, natomiast możesz zastanawiać się czym jest to modulo… to nic innego jak reszta z dzielenia. Na pewno w szkole podstawowej to było omawiane, ale o ile już wagarowałeś to Ci to z pomocą przyjdzie Ci ten portal. Może Ci się to wydawać zbędną operacją, ale nie martw się, jeszcze sie nam przyda.

Możemy też zmienną zwiększyć lub zmniejszyć o 1 poprzez dopisanie do niej odpowiednio ++ lub –. Dopisujemy to przed lub po zmiennej. Różnica jest taka, iż w przypadku, gdy wpiszemy przed zmienną to operacja zwróci nam liczbę zwiększoną/zmniejszoną o 1, natomiast gdy wpiszemy po zmiennej to zobaczymy tą samą liczbę co przed operacją. Obrazuje to poniższy przykład.

Pierwsze działania

“Dosyć pier***, robota czeka”

-Grucha, “Chłopaki nie płaczą”

Posłuchajmy więc Gruchy i do roboty. Zobaczysz zaraz na przykładzie prostego programu, jak definiować odpowiednio zmienne oraz jak wykonywać z ich użyciem operacje matematyczne. Zachęcam Cię również do wykonywania tego ze mną. W tym celu musisz stworzyć odpowiedni projekt (jak to zrobić omówiłem w poprzedniej części kursu). Zdefiniujemy najpierw zmienne każdego z typów, o jakich wspominałem, oraz przypiszemy im wartości:

Dla każdej liczby tutaj odbywa się deklaracja (czyli wpisanie zmiennej z jej typem) oraz inicjalizacja (przypisanie wartości). Zaczynamy od deklaracji, czyli mówimy jakiego typu zmienną tworzymy oraz podajemy nazwę po jakiej będziemy się do niej odwoływać. Następnie mamy znak “=”, który oznacza przypisanie wartości do danej zmiennej (w tym przypadku inicjalizację), a z prawej strony tego znaku jest wartość, którą chcemy przypisać. Operacje, jak każdą w Javie, kończymy znakiem średnika. Warto też wziąć pod uwagę, iż nazwy, jakich tu użyłem mogą być dowolne. Dobrą praktyką jednak jest, by mówiły poprzez nazwę co one tak naprawdę przechowują.

Możemy wykonać teraz operacje na zmiennych. Wyniki operacji jednak zachowamy w innych zmiennych oraz wypiszemy metodą System.out.println();, znaną nam już z naszego Hello Worlda (liczby też możemy wypisać bez problemu).

Operacje przebiegły bez problemu, program się wykonał. Twoją uwagę powinna zwrócić jednak operacja dzielenia. Spróbuj wykonać to dzielenie na kalkulatorze. Zobaczysz liczbę 8.75842049. Program natomiast wypisał okrągłe 8. Dlaczego? Otóż jak już wspomniałem, typy dla liczb stałoprzecinkowych nie przechowują części dziesiętnych liczby. Co więc dzieje się w takim momencie, gdy chcemy wynik takiej operacji w zmiennej dla liczb stałoprzecinkowych zachować? Java “ucina” część ułamkową. Nie ma żadnego zaokrąglania w górę czy w dół tam gdzie bliżej. Po prostu tniemy. o ile chcielibyśmy zaokrąglić to można użyć metody round() z biblioteki Math (opisana tutaj, sama biblioteka posiada dużo funkcji, których możesz używać. Zachęcam do zapoznania się z dokumentacją).

No dobrze, to zmieńmy typ zmiennej w której będziemy wynik przechowywać na float. Co się stanie?

Cóż, widzimy część ułamkową, ale nie jest ona poprawna…Java początkowo wykonuje operację na dwóch liczbach typu long. W związku z tym wynik operacji będzie zwrócony jako long właśnie. Przypisanie do zmiennej typu float owszem, sprawi, iż pod spodem będziemy mieć konwersję do typu float, ale już wcześniej “zgubiliśmy” ułamek. Nasz problem rozwiąże zmiana typu jednej z tych zmiennych na float. Java wtedy dokona konwersji i podzieli liczby zmiennoprzecinkowe. Ja zmieniłem typ dla myLong1 (nazwa trochę niefortunna w tym przypadku, ups…):

Jak widać, faktycznie pojawiła się część ułamkowa. o ile jesteś jednak uważny to zobaczysz iż coś tu nie gra… jest to inny wynik, który zwrócił nam kalkulator. W tym momencie wychodzą właśnie problemy z operacjami na liczbach zmiennoprzecinkowych. Nie zawsze będą one precyzyjne i trzeba tutaj bardzo uważać. Jak się przed tym bronić? Jednym z rozwiązań jest klasa BigDecimal, o której wcześniej wspomniałem. Poświęcę jej osobny wpis, ale oczywiście, zachęcam Cię do wcześniejszego zapoznania się z nią.

Jeszcze jest jeden interesujący przypadek. Co o ile podzielimy przez 0? W szkole Pani mówiła, iż nie wolno… no faktycznie. Operacja taka nam zwróci Exception, czyli wyjątek. Wyjątki są dużą częścią Javy, więc omówię je osobno. Haczyk jest taki, iż o ile chcesz zrobić takie zło, to najlepiej by była to liczba stałoprzecinkowa. W Javie 0 jako liczba zmiennoprzecinkowa jest zaimplementowana według standardu IEEE 754. Mówi on, iż dzielenie przez 0 w tym przypadku powinno nam zwrócić “nieskończoność”. Jak jest naprawdę?

Faktycznie, mamy nieskończoność… interesujący przypadek. Myślę, iż warto to wiedzieć. Możemy się (ponownie) nieprzyjemnie zaskoczyć…

Kolejność operacji

Podstawowe operacje na liczbach w Javie już umiesz wykonywać, cool! Przejdźmy do operacji trochę bardziej skomplikowanych. Zobaczmy, jak w zależności od zdefiniowania działania będą różnić się wyniki

W pierwszych dwóch przypadkach mamy dodawanie i mnożenie. Zapis kolejnych działań jest odwrócony, natomiast wynik mamy taki sam. Wynika to z tego, iż w pierwszej kolejności wykonujemy mnożenie, potem dodawanie. To się pokrywa z rzeczywistością.

W trzecim działaniu wynik się już zmienia. Różnica między działaniem pierwszym a trzecim jest taka, iż operację dodawania ujęliśmy w nawiasy. Powoduje to, iż działanie dodawania ma wyższy priorytet. I tak jest. Najpierw wykonujemy dodanie tych dwóch liczb, a następnie rezultat mnożymy, stąd inny wynik.

Wyniki działania czwartego i piątego mogą być ciekawe. Tutaj stosujemy dwa rodzaje inkrementacji. W pierwszym przypadku wynik działania wydaje się normalny. Zostanie wypisany wynik dodania trzech liczb. W drugim przypadku, gdy mamy inkrementację przed zmienną wynik jest większy. No dobra, ale dlaczego o 2, a nie o 1 skoro inkrementacja powinna liczbę zwiększyć o 1 właśnie? Otóż pierwsza inkrementacja wykonała się już w działaniu czwartym i zmienna została zwiększona. Jednak znaki ++ były po zmiennej, więc inkrementacja wykonała się “później”, czyli do działania wykorzystaliśmy pierwotną wartość. Druga inkrementacja odbywa się natomiast “wcześniej”. Wykonujemy już ją jednak na liczbie wcześniej zinkrementowanej, więc druga inkrementacja zwiększy ją po raz drugi, a iż odbywa się to wcześniej, to taką już właśnie liczbę użyjemy w działaniu, stąd wynik większy o 2 względem poprzedniego.

Operatory skrótowe

Być może powinienem o tym wspomnieć przy okazji operatorów “zwykłych”, jednak nie uważam tego tematu za istotny. Według wielu osób operatory takie pogarszają czytelność kodu. Mimo wszystko, warto wiedzieć czym taki operator jest i jak go użyć.

Operator skrótowy zwykle używany jest, gdy chcemy zmienić wartość zmiennej, np. poprzez dodanie do niej pięć. Pozwala on przejść z zapisu pierwszego do zapisu drugiego:

Wartości liczbowe obu działań będą takie same, zastąpiliśmy przypisanie wyniku działania poprzez zapisanie po zmiennej znaku operacji, którą chcemy na nią wykonać, znaku przypisania oraz liczbie, która ma być drugą częścią operacji. Pozwala więc nam to na zastąpienie zapisów zamieszczonych poniżej tymi, które są zamieszczone w odpowiadającym im komentarzach.

Podsumowanie i zadanie domowe

Myślę, iż to była dość spora dawka wiedzy dla początkującego programisty. Z liczbami wielokrotnie będziesz się mierzyć w Twojej przygodzie programisty (w końcu program to tak naprawdę ciąg zer i jedynek..:)), więc żeby być dobrym programistą to trzeba dobrze nimi “władać”.

Jako zadanie domowe, wykonaj działania zamieszczone poniżej oraz sprawdź, jaki jest ich rezultat poprzez wypisanie wyniku na konsolę. Za każdym razem spróbuj zmienić typ zmiennej, w której przechowywana jest wartość liczb na których działasz lub wynik (jeżeli jest to możliwe oczywiście). Następnie wykonaj te same działania na kalkulatorze.

0.1 + 0.2 2147483647 + 1 5 / 0.000001 2.0 / 2.0 2 / 2 3 % 0 1 - 2147483650

Czy wyniki się pokrywają? Dlaczego czasem może to być nieprawda? Wyciągnij wnioski z tych działań. o ile masz pytania, widzisz błąd, nie krępuj się i zostaw komentarz!

<- [#01] – Hello, World! [#03] – Znaki tekstowe ->

Idź do oryginalnego materiału