Czym się różni JRE od JDK?

Świat Java pełen jest trzyliterowych skrótów: JDK, JVM, JRE, SDK, JSF, JMX… i tak dalej. Dwa najbardziej podstawowe akronimy to JRE oraz JDK. Często zdarza się, że są one mylone. Wyjaśnijmy sobie, raz na zawsze co one oznaczają oraz czym się różnią. Zacznijmy od JRE!

JRE, czyli Java Runtime Environment (środowisko uruchomieniowe Java). Programy napisane w Java, nie będą działały bezpośrednio na komputerze. Do ich uruchomienia potrzebna jest maszyna wirtualna (JVM; więcej na ten temat przeczytasz we wpisie: jak działa Java). Żeby móc uruchomić aplikację w Java trzeba posiadać maszynę wirtualną, czyli zainstalowane JRE na swoim komputerze. W skład JRE wchodzą wszystkie komponenty potrzebne do uruchomienia aplikacji Java. JRE instalowane jest na komputerach użytkowników końcowych aplikacji oraz na serwerach udostępniających aplikację w formie strony WWW w internecie.

JDK, czyli Java Development Kit (zestaw developera Java). Jest to takie “JRE” na sterydach, gdyż oprócz wszystkiego tego, co znajduje się w JRE, dostarcza również narzędzia potrzebne do stworzenia oprogramowania takie, chociażby jak kompilator oraz inne umożliwiające min. analizę działania aplikacji. Bez JDK nie będziesz w stanie stworzyć aplikacji Java… chociażby dlatego, że nie będziesz, mógł skompilować kod bez kompilatora ;). Dlatego właśnie JDK instalowane jest na komputerach programistów piszących oprogramowanie w Java.

Podsumowując, JRE jest dla użytkowników końcowych, pozwala uruchomić oprogramowanie stworzone przez programistów. Natomiast JDK jest dla programistów, po to, żeby mogli tworzyć oprogramowanie.

Co to jest i jak czytać StackTrace?

Wiesz już, czym są wyjątki w Java, wiesz też jak je rzucać… ale czy wiesz jak je interpretować? W szczególności jak czytać StackTrace, który zawsze towarzyszy wyjątkowi?

Wśród programistów krąży powiedzenie, że kod jest czytany więcej razy niż pisany (dlatego musi być napisany w prosty i zrozumiały sposób). Podobnie jest z wyjątkami, częściej będziesz je czytał niż, rzucał w swojej aplikacji. Dlatego też warto nauczyć się je czytać zawczasu i nie domyślać się “co tak naprawdę to znaczy”.

Zanim zapoznasz się z samym StackTrace’em warto, żebyś wiedział, że stuktura danych, na której on bazuje, zwana jest stosem (ang. stack). Opisałem ją dokładnie w tym wpisie.

Można powiedzieć, że StackTrace to “ścieżka”, którą podążało wykonanie programu do momentu powstania błędu. Więc w StackTrace znajduje się lista wszystkich metod, które są „aktualnie” wykonywane i w takiej kolejności doprowadziły do wyjątku.

Mówiąc jeszcze inaczej, patrząc na StackTrace, będziesz widział listę metod wywołanych na poszczególnych poziomach aplikacji. Wywołania te doprowadziły do wystąpienia tego wyjątku. Co ważne, nie zobaczysz tutaj wartości zmiennych, oraz wartości atrybutów (pól) klasy.

Samo pojęcie StackTrace związane jest ze stosem wykonania, jednak w Java potocznie rozumie się przez to sam stos wykonania wraz z wyjątkiem (lub wyjątkami), w jaki jest on “opakowany”.

Javowy StackTrace, zaczyna się od nazwy wyjątku, który go spowodował. W tej samej linii znajduje się też dodatkowa informacja dołączona przez programistę. “Zazwyczaj”, gdyż sporo osób nie dołącza do wyjątku żadnej “wiadomości”. Przyśpiesza to i ułatwia programowanie, bo nie trzeba się zastanawiać co tam napisać. Jednak jest to jedyny sposób, w jaki możemy sobie sami pomóc, kiedy to przyjdzie nam analizować ten wyjątek. W tej wiadomości możemy dodać informacje o wartości lokalnych zmiennych, które wpłynęły na wystąpienie właśnie tego wyjątku. Dlatego zawsze dodawaj “wiadomość” do każdego wyjątku, jaki rzucasz.

Każda linia zaczynająca się od “at” podaje:

  • w pełni kwalifikowaną nazwę metody (czyli nazwę pakietu, klasy oraz metody),
  • nazwę pliku źródłowego (w nawiasach okrągłych) oraz,
  • po dwukropku, numer linii.

Taki zapis pozwala jednoznacznie określić miejsce w aplikacji (lub bibliotece zewnętrznej), które było aktualnie wykonywane.

Na samym dole znajduje się tzw. metoda wejściowa, w prostych aplikacjach konsolowych, będzie to zawsze metoda main(String []). Natomiast na samym końcu znajduje się metoda, w której to wystąpił wyjątek.

Zazwyczaj nazwa pakietu Twojej aplikacji będzie znajdowała się na samej górze… gdyż zazwyczaj błędy popełniamy my, a nie autorzy bibliotek, których używamy. Czasami jednak nasza nazwa pakietu znajduje się gdzieś w “środku” StackTrace, wtedy zazwyczaj oznacza to, że przekazaliśmy złe parametry do wywołania metody z danej biblioteki. Baaaardzo rzadko oznacza to błąd w bibliotece.

Przyjrzyjmy się przykładowemu wyjątkowi:

Widzimy tutaj NullPointerException w 16 linii klasy Book z pakietu pl.kursprogramisty w metodzie getTitle(). Metoda ta została wywołana przez Author.getBookTitles(), która znajduje się w pliku Author.java w linii 25. Z kolei metoda getBookTitles() wołana jest z Bootstrap.main(), z pliku Bootstrap.java w linii 14.

Jak to bywa zazwyczaj z wyjątkami typu NullPoinerException, pewnie jedno z pól (właściwości) klasy Book bądź nie zostało zainicjalizowane, bądź zostało zainicjalizowane wartością null (czyli, tak jak by w ogóle nie zostało zainicjalizowane). Takie zachowanie skutkuje właśnie takim wyjątkiem w momencie próby wykonania jakiejkolwiek metody, na której zmiennej wartość jest null.

Często widzi się w kodzie, że w bloku catch, złapany wyjątek opakowywany jest w inny typ, który jest następnie rzucany. Poniżej znajduje się przykład takiego kodu:

W takim przypadku należy rozumieć, że wyjątek MyProjectServletException został spowodowany przez (ang. caused by) ConstraintViolationException. Wtedy w StackTrace zobaczymy linię zaczynającą się od “Caused by: ConstraintViolationException”. Co może skutkować takim przykładowym StackTrace:

Przełóżmy to na trochę prostszy przykład. Załóżmy, że piszesz prosty kalkulator:

W powyższym przykładzie CalculationException spowodowany został przez AdditionException, gdyż próbujemy dodać dwie liczby, z których jedna jest spoza karesu przyjętego przez nas za obsługiwany.

To tyle, jeżeli chodzi o czytanie wyjątków ze zrozumieniem. Jeżeli coś jest jeszcze nie jasne, zapraszam do komentarzy.

Co to jest Stos (ang. stack)?

Mentos

Źródło: https://www.flickr.com/photos/hellokitae/4291992600

Jedną z podstawowych struktura danych w informatyce jest stos (ang. stack). Chociaż nie jest on często stosowany w kodzie aplikacji (częściej spotkasz się z listami, setami oraz mapami) to bez niego nie funkcjonowałaby maszyna wirtualna Javy oraz komputery.

Dlaczego? Otóż stos wykorzystywany jest w celu “zlezienia drogi powrotnej” po wykonaniu metody, funkcji czy też instrukcji procesora. W momencie wywołania metody “A” adres aktualnego miejsca w programie jest odkładany na stos wywołania programu, następnie wykonywane są instrukcje z metody “B” (jeżeli w jej ciele odwołujemy się do kolejnej metody (“C”) to odkładamy na stos adres metody “B”, żeby wykonać metodę “C”). Kiedy metoda “B” zostanie wykonana, do końca JVM pobiera adres, do którego ma wrócić ze stosu wykonania… i wraca, zwracając wartość wyliczoną przez metodę “B”. Tak w sporym uproszczeniu można opisać, jak działa JVM.

Innym bardziej “namacalnym” zastosowaniem stosu jest tzw. StackTrace (tzn. stos wywołań) który możesz zobaczyć na ekranie w momencie wystąpienia wyjątku (ang. exception) w programie.

Jak sobie wyobrazić taki stos? Wyobraź sobie (lub po prostu kup) Mentos’a najlepiej “wieloowocowego”. Jest on tak zapakowany, że nie możesz dostać się do dowolnego cukierka. Żeby wsiąść np. czwarty, musisz najpierw wyciągnąć pierwsze trzy. Tak samo nie wiesz też jaki ten czwarty cukierek będzie. Jedyne co możesz zrobić to “zajrzeć”, jaki będzie następny, który będziesz mógł wyciągnąć.

Zostało jeszcze nam zastanowić się nad wkładaniem cukierków. Taka czynność jest bardzo utrudniona w rzeczywistości (raz wciągnięty cukierek ciężko jest włożyć ponownie). Jednak jeżeli użyjemy trochę wyobraźni, to zauważymy, że nie możemy włożyć cukierka na dowolną pozycję, zawsze musimy włożyć go na koniec (początek) “kolejki”.

Tak samo jest ze stosem w programowaniu. Przede wszystkim zachowuje on kolejność, w jakiej zostały do niego włożone elementy, ale pozwala je pobrać w kolejności odwróconej (pierwszy, który został włożony, zostanie pobrany jako ostatni). Czyli zawsze mamy szybki dostęp do elementu, który został włożony jako ostatni. Dodatkowo możemy podejrzeć, jaki będzie następny element możliwy do pobrania.

Przejdźmy teraz do tego, jak w Java używać stosów. JVM dostarcza nam implementację stosu pod postacią sparametryzowanej (lub generycznej) klasy java.util.Stack. Trzy najważniejsze metody pozwalające nam używać stosu to:

  • push(element) – pozwala nam włożyć element na “wierzchołek” stosu,
  • pop() – ta metoda wykonuje tak naprawdę dwie operacje. Po pierwsze “ściąga” (usuwa) ostatni element ze stosu oraz zwraca jego wartość (czyli tak jak by wyciągnąć cukierek z opakowania Mentosów),
  • peek() – pozwala nam podejrzeć ostatni element, jaki znajduje się w stosie. Wywołanie tej metody zwróci ten sam obiekt co pop(), z tym że nie usunie go ze stosu (czyli jest to takie zajrzenie do środka Mentosa, żeby sprawdzić jaki cukierek będzie następny).

Dodatkowo dostępne mamy jeszcze:

  • search(element) – pozwala znaleść dany element w stosie. Zwróci nam “numer” pozycji, na której znajduje się podany obiekt (element) lub wartość -1, gdy ten obiekt nie znajduje się w secie,
  • empty() – pozwala sprawdzić, czy stos jest pusty.

Przykład wykorzystania stosu:

Wynikiem wykonania tego kodu będzie:

W ten oto sposób używa się klasy java.util.Stack. Gdyby coś było jeszcze nie jasne, zapraszam do zadawania pytań w komentarzach.

Czym są struktury danych?

Zaczynając przygodę z programowaniem, warto zaznajomić się z kilkoma podstawowymi pojęciami, które są uniwersalne dla wszystkich języków programowania. Jednym z takich pojęć jest właśnie struktura danych. Nie ważne czy będziesz programował w pythonie, c, c++, java script, swift czy objective c zawsze spotkasz się z tym pojęciem. Zastosowanie odpowiedniej struktury danych może przyśpieszyć działanie aplikacji (oczywiście zły wybór takowej może ją spowolnić). Każdy język programowania dostarcza kilka podstawowych struktur danych, dodatkowe mogą zostać dostarczone przez zewnętrzne biblioteki.

Więc czym właściwie są te struktury danych? Najprościej mówiąc, jest to sposób, w jaki przechowywane są dane w pamięci komputera. Każdy z tych sposobów ma swoje plusy i minusy, niestety nie można mieć wszystkiego :).

Jak można sobie wyobrazić takie różne struktury danych? Wyobraź sobie 50 książek oraz cztery osoby z różnym podejściem do kolejności ich czytania.

  • Tomek trzyma książki na kupce w kącie pokoju. Każdą nową pozycję po prostu kładzie “gdzieś” losowo na tej właśnie kupce. Kiedy chce przeczytać kolejną książkę, po prostu bierze pierwszą lepszą z brzegu i czyta.
  • Marian, z drugiej story, woli czytać książki takiej kolejności, w jakiej je kupił. Więc trzyma je ułożone równo na półce. Nowe pozycje dokłada zawsze na końcu, a kiedy chce przeczytać kolejną książkę, bierze po prostu tą “najstarszą”. Dodatkowo Marian może szybko sprawdzić jaką książkę przeczyta następną, patrząc na koniec swojej kolejki ;).
  • Ania, podobnie jak Marian trzyma książki równo ułożone na półce, z tym że gdy chce przeczytać kolejną, zgaduje sobie jakąś liczbę od 1 do 50. Potem odlicza ją od prawej strony i wyciąga książkę z tej pozycji.
  • Gosia z kolei czyta książki bazując na rekomendacjach znajomych. Pyta ich o kolejny tytuł do przeczytania, a potem na podstawie tego tytułu odnajduje daną książkę na półce i ją czyta.

Każde z powyższych podejść można zaimplementować z wykorzystaniem prostej tablicy. Niestety przy każdym z nich będziemy musieli się “namęczyć” (tj. napisać dużo wlanego kodu), żeby osiągnąć dany efekt.

Pierwszy problem, jaki napotkamy przy każdej implementacji to to, że tablica ma określny rozmiar, którego w Java nie można zmienić “w locie”. Trzeba najpierw zdeklarować nową tablicę o większym lub mniejszym rozmiarze, a potem przepisać wszystkie elementy (plus lub minus jeden) do nowej tablicy.

  • W przypadku Tomka dodatkowo będziemy musieli losować liczbę po to, żeby wiedzieć, w które miejsce włożyć nową książkę oraz żeby “wiedzieć” jaką książkę przeczytać następną.
  • Jeżeli chodzi o Mariana, to zawsze będziemy przepisywać naszą tablicę książek. Przy każdym dołożeniu nowej książki będziemy musieli zwiększyć jej rozmiar o jeden i dodać na końcu nową pozycję. Natomiast w przypadku wzięcia nowej lektury do czytania, będziemy brać tą z pozycji 0, a potem usuwać ją z tablicy… czyli znowu musimy przepisać całość, tym razem bez pierwszego elementu.
  • Podobnie jest w przypadku Ani, z tym że nie wiemy którą pozycję weźmie ona jako następną, więc nasza operacja przepisania będzie jeszcze bardziej skomplikowana.
  • Natomiast w przypadku Gosi, najlepiej będzie nam mieć drugą tablicę, w której będziemy trzymać tytuły książek w takiej samej kolejności, w jakiej są one w tablicy, która przechowuje “całe” książki… oczywiście w tym przypadku musimy “opiekować” się obiema tablicami oraz pilnować, żeby nie rozjechała się nam kolejność.

Tak jak pisałem wcześniej, wszystko da się zrobić, tylko po co się męczyć… jeżeli wiemy, co chcemy osiągnąć oraz znamy właściwości kilku struktur danych, powyższe problemy rozwiążemy, stosując odpowiedni typ zmiennej w Java (czyli wybierając odpowiedni typ danych). Stosując odpowiednią strukturę danych, nie musimy pisać sami kodu związanego z dodawaniem, usuwaniem oraz pobieraniem elementów. Dodatkowo nie musimy się też martwić o rozmiar danej struktury. Wszystkie te rzeczy zrobione są za nas przez innych programistów. Dodatkowo są one przetestowane oraz zaimplementowane w najbardziej wydajny sposób.

Korzystając ze struktur danych dostępnych w Java, biblioteczki naszych poszczególnych bohaterów można zaimplementować używając:

  • java.util.HashSet – korzystamy tutaj ze struktury danych znanej jako set, która to jest takim workiem, do którego można bardzo szybko coś włożyć, oraz szybko wyciągnąć losowy element. Jeżeli nie obchodzi nas kolejność elementów oraz nigdy nie będziemy chcieli pobrać konkretnego elementu jest to idealna struktura danych.
  • java.util.Stack – czyli stos. Jego podstawowymi właściwościami jest to, że dodawane elementy zawsze lądują na końcu oraz to, umożliwia on łatwy (szybki) dostęp do najstarszego elementu (tego, który został umieszczony tam jako pierwszy) oraz można podejrzeć (bez pobierania/ściągania ze stosu) element, jaki jest na samej górze (tj. najstarszy włożony).
  • java.util.ArrayList – struktura danych, z jakiej korzystamy tutaj to lista. Umożliwia ona szybki dostęp do dowolnego elementu po jego liczbie porządkowej. Porządek w liście trzymany jest według kolejności wkładania elementów tj. kolejne elementy dodawane są na końcu listy.
  • java.util.HashMap – czyli mapa… umożliwia ona mapowanie (łączenie) dowolnego elementu w formie klucz-wartość. W przypadku Gosi kluczem będzie tytuł książki (typ String), a wartością obiekt samej książki.

Reasumując, czym są struktury danych? Jest to sposób, w jaki dane przechowywane są w pamięci komputera, oraz algorytmy umożliwiające wykonywanie operacji takich jak dodawanie, usuwanie oraz pobieranie elementów w najbardziej wydajny sposób. Zastosowanie odpowiedniej struktury danych może przyśpieszyć działanie aplikacji oraz zmniejszy ilość kodu, jaki musimy napisać w celu osiągnięcia działania aplikacji. Biblioteka standardowa Java zawiera wiele różnych struktur danych, jako dobry programista powinieneś znać je wszystkie plus dodatkowo kilka dostarczanych przez zewnętrzne biblioteki jak na przykład Google Guava.

Wyjątki w Java: NullPointerException

NullPointerExcepion to chyba najczęściej spotykany wyjątek w Java, co więcej wynika on zazwyczaj z błędu programisty. Oznacza on przeważnie wywołanie metody na obiekcie, który ma wartość null.

Często błąd ten występuje, kiedy przez pomyłkę jeden z parametrów wykonywanej metody ma wartość null. OK, ale co to właściwie znaczy? Przyjrzyjmy się prostemu przykładowi:

Żeby nadmiernie nie komplikować kodu, opakowujemy standardową metodę concat z klasy String w naszą prywatną metodę concatenate, która przyjmuje dwa parametry typu String.

Jeżeli uruchomimy, aplikację dostaniemy następujący rezultat:

Oto jest i on, słynny NullPointerException! Zacznijmy od ostatniej linii tj.: at pl.naukajava.NullPointerExceptionExample.main(NullPointerExceptionExample.java:7), która to wskazuje, że błąd występuje w 7 linii naszej aplikacji tj.:

Kolejna linia w stack trace wskazuje nam na naszą metodę concatenate(String, String). Natomiast tutaj: at java.lang.String.concat(String.java:2027) mamy odwołanie do standardowej metody concat z klasy String.

Co z tego wszystkiego wynika? Otóż metoda concat nie działa jeżeli przekażemy jej wartość null jako parametr. Jak to poprawić? Jest wiele możliwych rozwiązań:

  1. zamienić wartość null na pusty String
  2. zamienić wartość null na String “null”
  3. w metodzie concatenate dodać warunek if (b == null) i wtedy podmienić tą wartość na pusty String lub String “null”
  4. w metodzie concatenate dodać warunek if (b == null) i rzucić wyjątek IllegalArgumentException

Oczywiście dobre rozwiązanie będzie się różnić od tego, jaki efekt chcemy osiągnąć. Ja, osobiście składniam się do 3 propozycji i zamiany wartości null na String “null”.

Nowa wersja metody concatenate będzie wyglądała następująco:

Jeżeli teraz uruchomimy ponownie tą aplikację, to znowu otrzymamy NullPointerException, tym razem w linii 8… no i tym razem sami jesteśmy sobie winni. Gdyż nie sprawdzamy wartości zmiennej a tylko bezpośrednio wywołujemy na niej metodę concat, co w rezultacie daje nam NPE (akronim od NullPointerException).

Ponownie wypadało by się zabezpieczyć przed tym błędem. Najlepiej w sposób konsystentny z tym w jaki poprawiliśmy pierwszy błąd tj. zamienić null na String “null”. Mamy już kod który wykonuje dokładnie taką operację dla zmiennej b, więc albo możemy go skopiować (źle, źle, źle, źle..,), albo wyciągnąć go do nowej metody np. nullToString.

Musimy teraz zmienić metodę concatenate, żeby korzystała z nowo utworzonej metody nullToString:

Teraz nasza mała aplikacja demonstracyjna będzie działa tak bez problemów.

W ten oto sposób pozbywamy się z aplikacji błędów typu NullPointerException.