#03 – konwersja typów, obsługa wyjątków

By 28 sierpnia 2014Kurs Javy
Nagłówek do lekcji #03

W tej lekcji wypełnimy nasz obiekt kota brakującymi danymi – datą oraz masą. Jest to trudniejsze zadanie, ponieważ użytkownik może wpisać cokolwiek (np. „abcde”) a niekoniecznie musi to być poprawna data / liczba.

W dalszych lekcjach nauczymy się bardziej poprawnego sposobu (używając wyrażeń regularnych), na tę chwilę wykorzystamy możliwości obsługi błędów (wyjątków) które daje nam język Java (miejmy na uwadze, że nie jest to dobre podejście – powinniśmy nie dopuścić do pojawienia się błędu, a nie go obsługiwać; zajmiemy się tym w kolejnych lekcjach).

Lekcja

W tej lekcji nauczymy się, jak używać pętli do-while, jak obsługiwać błędy (wyjątki) oraz jak konwertować daty.

Pętla do-while

Pętla do while służy do powtarzania pewnej czynności dopóki określony warunek jest prawdziwy. To, co cechuje tę konstrukcję to gwarancja, że kod wewnątrz pętli zostanie wykonany co najmniej raz. Konstrukcja pętli wygląda następująco:

do {
    rozne_czynnosci();
} while (warunek_logiczny);

rozne_czynnosci() to oczywiście jakikolwiek kod, który chcemy wykonać (oczywiście może to być wiele instrukcji, a nie tylko jedna).

Instrukcja sterująca break

Może się zdarzyć sytuacja, w której niezależnie od warunku while () chcemy przerwać pętlę dokładnie w tym momencie nie wykonując nawet pozostałych instrukcji. Zobaczmy na poniższy przykład (oczywiście ktoś może mieć inne zdanie ;) ):

do {
    Potrawa potrawa = wezZTalerzaKolejnaPotrawe();
    if (potrawa.getName().equals("brukselka") {
        break;
    }
    potrawa.zjedz();
} while (czyJestesGlodny());

 

W pętli tej, bierzemy kolejną potrawę z talerza i zjadamy ją dopóki jesteśmy głodni. Jesli jednak trafimy na brukselkę, natychmiast przestajemy jeść. Używanie tej konstrukcji jest niezalecane, ponieważ nie jest bardzo czytelne i może wprowadzać w bład innych programistów pracujących z naszym kodem, czasem jest ono jednak niezbędne.

Instrukcję tą możemy używać w każdym rodzaju pętli w Javie (do-while, while, for).

Obrazowe działanie instrukcji break

Obrazowe działanie instrukcji break

Instrukcja sterująca continue

Podobnie jak powyższa instrukcja, może ona być używana w dowolnej pętli (ale nigdy poza nią). Różnica polega na tym, że continue pomija obecny krok i przeskakuje do końca bloku instrukcji (w przypadku pętli do-while, do sprawdzenia warunku). Modyfikując wcześniejszy przykład w następujący sposób:

do {
    Potrawa potrawa = wezZTalerzaKolejnaPotrawe();
    if (potrawa.getName().equals("brukselka") {
        continue;
    }
    potrawa.zjedz();
} while (czyJestesGlodny());

Tym razem nie przerwiemy posiłku po natrafieniu na brukselkę, a jedynie pominiemy jej zjedzenie (jeśli trafimy na brukselkę to od razu ponownie sprawdzimy, czy nadal jesteśmy głodni).

Obrazowe działanie instrukcji continue

Obrazowe działanie instrukcji continue

Wyjątki i ich obsługa

Wyjątki w Javie to specjalne obiekty które mogą być nie tyle zwracane, co rzucane. Sygnalizują one sytuację niecodzienną, która nie powinna mieć miejsca. Ponieważ obsługa wyjątków powoduje duże zmiany w sposobie wykonywania kodu naszej aplikacji, nie należy ich używać do sygnalizowania normalnych sytuacji lub przekazywania informacji (co w pewnym stopniu zrobimy w tej lekcji, ale poprawimy się w następnej!) – należy zawsze w miarę możliwości sprawdzić, czy wszystkie warunki są spełnione żeby wyjątek nie został rzucony.

Wyjątki dzielimy na dwa typy – te, które musimy obsłużyć (tzw. checked exception) oraz te, których obsługiwać nie musimy, ale możemy (unchecked – te, które dziedziczą po klasie RuntimeException; o dziedziczeniu i jego znaczeniu w Javie powiemy szerzej już wkrótce).

Pierwszy rodzaj wyjątków to także takie, które sami tworzymy i rzucamy. Rzucanie wyjątków dotyczy sytuacji w których programujemy elementy bardzo niskopoziomowe (np. obsługę protokołu) i raczej nie będziesz miała potrzeby robić tego w najbliższym czasie (w swojej karierze od kilku lat nie miałem sytuacji, która wymagałaby rzucenia wyjątku przeze mnie w kodzie), dlatego pominiemy to.

Drugi rodzaj wyjątków to te, które obsługiwać możemy, ale nie musimy (i cokolwiek by się działo – nie tworzymy sami tego rodzaju wyjątków!). To są wyjątki, które mogą wystąpić w miejscach gdzie byśmy tego nie oczekiwali (to duże uproszczenie i nie do końca prawdziwe, ale nie wiedziałem jak to napisać poprawnie i zwięźle, a nie jest to bardzo istotne :) ) i kod stałby się bardzo nieczytelny i wymagałby dużo kodu do samej obsługi wyjątków. Przykładem takiego wyjątku jest NullPointerException który teoretycznie mógłby się pojawić w każdym miejscu, gdzie wywołujemy jakąś metodę (czyli prawie wszędzie).

Jeśli chcemy zobaczyć jaki wyjątek może rzucić używana przez nas metoda zaglądamy do dokumentacji. Oczywiście środowisko Eclipse samo podpowie nam jakie wyjątki musimy obsłużyć. Ale nauczmy się przy okazji korzystać z dokumentacji (nawiasem mówiąc – bardzo dobrej) języka Java.

Zobaczmy dla przykładu dokumentację dla metody Integer.valueOf(String string) (http://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#valueOf(java.lang.String)), która zamienia ciąg znaków w liczbę (opis, co robi ta metoda, także znajdziemy w jej dokumentacji). W dokumentacji na dole jest sekcja ‚Throws’, która wypisuje wyjątki jakie metoda może zwrócić oraz krótko opisuje sytuację, w której są one rzucane. Akurat w tym wypadku jest to wyjątek NumberFormatException, który jest unchecked (po kliknięciu na niego możemy to sprawdzić – na samym początku jest informacja o tym, że dziedziczy po RuntimeException, ponieważ klasa ta jest wypisana w ‚drzewku’).

Możemy więc spokojnie napisać poniższy fragment:

Integer liczba = Integer.valueOf("jeden");

I skompiluje się on poprawnie, natomiast próbując uruchomić taki program, otrzymamy wyjątek w trakcie działania programu (ponieważ go nie obsłużyliśmy). Nauczmy się obsługiwać więc wyjątki za pomocą bloku try-catch-finally, który ma następującą budowę:

try{
    //kod który może rzucić wyjątek
} catch (TypWyjatku wyjatek) {
    //kod, który wykona się, kiedy wystąpi wyjątek
} finally {
    //kod, który wykona się zawsze, niezależnie czy wystąpi wyjątek czy nie
}

Jeśli chodzi o formalne wymogi, to wymagana jest część try {}, oraz część finally {} lub jedna (lub więcej) część catch (TypWyjatku nazwaZmiennej) {} . Korzystając z tych informacji, poprawne są wszystkie poniższe przykłady:

Integer liczba = null;
try {
    liczba = Integer.valueOf(jakisCiagZnakow);
} catch (NumberFormatException nfe) {
    System.out.println("Wyjątek - zły format liczby");
} catch (Exception e) {
    System.out.println("Wyjątek, ale jakiś inny");
} finally {
    System.out.println("To się zawsze wykonuje");
}
Integer liczba = null;
try {
    liczba = Integer.valueOf(jakisCiagZnakow);
} finally {
    System.out.println("To się zawsze wykonuje");
}
Integer liczba = null;
try {
    liczba = Integer.valueOf(jakisCiagZnakow);
} catch (NumberFormatException nfe) {
    System.out.println("Wyjątek - zły format liczby");
}
Integer liczba = null;
try {
    liczba = Integer.valueOf(jakisCiagZnakow);
} catch (NumberFormatException nfe) {
    System.out.println("Wyjątek - zły format liczby");
} catch (Exception e) {
    System.out.println("Wyjątek, ale jakiś inny");
}

W ramach ćwiczenia wyjaśnij, jakie są różnice pomiędzy nimi – odpowiedź napisz w komentarzu. Dla autora najlepszego wyjaśnienia bonus-niespodzianka ;)

Konwertowanie daty

Do konwersji ciągu znaków do daty możemy użyć np. klasy SimpleDateFormat, która oferuje bardzo wiele możliwości. Aby ją utworzyć, musimy podać wzorzec daty, z którym będziemy pracować. Wzorzec ten określa co występuje w jakiej kolejności, oraz w jakim jest formacie. Klasy tej możemy używać także do wypisywania daty we wskazanym formacie. Zacznijmy od prostego przykładu:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");

Co oznaczają poszczególne literki pozwolę Ci samej odszyfrować z pomocą dokumentacji tej klasy :)

Jeśli mamy ciąg znaków i chcemy zamienić go w datę musimy użyć metody parse, która przyjmuje ciąg znaków i zwraca obiekt typu java.util.Date. Metoda ta może rzucić wyjątek, który musimy obsłużyć, przykładowe użycie wygląda więc następująco:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
Date date = null;
try {
    date = sdf.parse(dataJakoCiagZnakow);
} catch (ParseException pe) {
    System.out.println("Coś jest nie tak z formatem daty!");
}
//tu sprawdzamy

Jeśli dataJakoCiagZnakow będzie w dobrym formacie, w miejscu gdzie jest komentarz ‚tu sprawdzamy’ zmienna date będzie miała przypisany obiekt typu java.util.Date przechowujący naszą datę. Jeśli jednak nasz ciąg znaków będzie w innym formacie niż określiliśmy w SimpleDateFormat, metoda parse zwróci wyjątek, a zmienna date nadal będzie miała wartość null (podpowiedź: przyda się to w zadaniu do dzisiejszej lekcji).

Jak sama widzisz, zamiana ciągu znaków w datę jest banalnie prosta i w łatwy sposób możemy zbudować własny format daty, jaki chcemy wczytywać.

Zadanie

Popraw klasę z poprzedniego zadania tak, aby:

  • odczytywała i zapisywała w obiekcie kot datę w formacie RRRR-MM-DD
  • odczytywała i zapisywała w obiekcie kot jego masę w formacie zmiennoprzecinkowym

Jeśli walidacja któregoś pola się nie powiedzie, program powinien pytać o poprawne dane aż do skutku (zanim przejdzie dalej).

W ramach zadania do samodzielnego czytania, zapoznaj się z materiałami o pętli while a nastepnie odpowiedz na poniższe pytania:

  1. Jak wygląda budowa pętli while? Czy można jej użyć w powyższym zadaniu? Czy zmieni to sposób działania?
  2. Czym różni się pętla while od do-while?

zip Pobierz rozwiązanie tego zadania

#3 konwersja typów, obsługa wyjątków

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • Agnieszka

    Kurs jest super, ale tutaj miałam problem, żeby zrozumieć skąd w zadaniu wzięło się w pętli: rób coś tam, dopóki kot.getDataUrodzenia() == null. Na początku myślałam, że to po prostu oznacza, że jeśli użytkownik nic nie wpisze, to będzie się pętla wykonywać, ale może (jeśli dobrze zrozumiałam) warto byłoby zaznaczyć, że tu chodzi o wskaźnik obszaru pamięci, tzn. że w przypadku null nie wskazuje na żaden obiekt. Chyba, że później coś o referencjach jest, bo ja zaczynam od początku :) A może to i dobrze, że trzeba trochę samemu poszukać, żeby znaleźć odpowiedź. Tak czy siak, miałam niezłą zagwozdkę, ale już przechodzę dalej ;)

  • Przemko

    Jeżeli damy taki warunek w pętli (przykład z rozwiązania ze strony)
    kot.getWaga() == null
    To program się nie skompiluje…
    Log: The operator == is undefined for the argument type(s) float, null
    Z tego co mi się wydaję to żaden typ liczbowy nie może być null…..
    W takim razie jest błąd w rozwiązaniu…

    • Faktycznie, powinno być Float. Poprawimy i uzupełnimy, dziękujemy za zwrócenie uwagi!

  • Michal1511

    Kompletnie nie rozumiem jak mam wykonać tutaj to zadanie:(
    Dodałem wpis, który wyświetla mi aktualną datę pobieraną z komputera, ale nie wiem czy o to chodziło.
    Bo nie wiem jak zrobić, żeby zczytywał mi to co wpiszę i wyświetlił to co wpisałem.

    Kod z datą:

    http://pastebin.com/4Rfkac7X

    • W zadaniu w kwestii dat chodzi o to, żeby wczytać od użytkownika aplikacji ciąg znaków, i zamienić go na datę. Zerkniji do rozdziału ‚Kon­wer­towanie daty’ w tej lekcji, a jeśli nadal będziesz miał problem – zerknij też do rozwiązania tej lekcji.

      • Michal1511

        Dziękuje za wyjaśnienie. Posiłkowałem się waszym rozwiązaniem niestety.
        Jednak nie ma czegoś takiego w programie, że jak toś wpisze inny format daty, że wyświetli napis błędu i poprosi o ponowne wpisanie, dlaczego?

  • Karol

    Wszystko super. Nie rozumiem tylko struktury tworzenia plików, dlaczego w rozwiązanym przykładzie zostało to podzielone na 2 paczki a nie np klase Kot i Main w jednej paczce. Jak tworząc aplikacje stosować się do mvc?

    • O MVC mówimy nieco dalej – w lekcji 9 (http://kobietydokodu.pl/09-spring-mvc/).

      Podział na pakiety wynika z tego, że dzielimy aplikacje na logiczne części – jedna część to tzw model lub obiekty domenowe – wszystkie te obiekty, które przetwarzamy w naszej aplikacji, a druga to sama aplikacja i jej logika biznesowa. Podziału dokonujemy aby pokazać, że są to różne rzeczy i w skomplikowanej aplikacji dzięki temu dużo łatwiej będzie nam odnaleźć prawidłowe elementy.

  • Monika Senderecka

    Cześć, czy jest sens wprowadzania dwóch bloków cach jeśli obsłużyliśmy już wyjątek który wyrzuca metoda? jeśli tak to w jakich przypadkach miało by to zastosowanie? trudno mi to sobie wyobrazić na tym przykładzie, a jest to moja pierwsza styczność z wyjątkami.

  • Marek

    Witam,

    rozwiązałem zadanie w nieco inny sposób, niż ten wskazany przez Was w pliku do ściągnięcia. Mój kod wygląda tak (zamieszczam tylko tę jego część, która dotyczy daty i wagi):

    Date date = null;
    do {
    System.out.println(„Enter the cat’s birth day e.g. 2012-06-25:”);
    try {
    date = sdf.parse(getUserInput());
    } catch (ParseException e) {
    System.out.println(„Something went wrong! (follow given example 2012-06-25)”);
    }
    } while (date == null);
    kot.setDate(date);

    Float waga = null;
    do{
    System.out.println(„Enter the cat’s weight in floating point value: (e.g. 12.56) „);

    try{
    //waga = Float.parseFloat(getUserInput());
    waga = Float.valueOf(getUserInput());
    } catch (NumberFormatException e){
    System.out.println(„Wrong value! (follow given example 12.56)”);
    }
    }while (waga == null);

    kot.setWeight(waga);

    Moje pytanie jest następujące, dlaczego Wasze rozwiązanie jest lepsze? Jedyne co przychodzi mi na myśl, to dodatkowa zbędna zmienna w moim kodzie, a nawet dwie – odpowiednio jedna dla Date date = null; i druga Float waga = null.

    Bardzo dziękuję za odpowiedź, jak również za to, że zamieszczacie te lekcje. Świetna robota!

    • Nasze rozwiązanie nie jest lepsze – jest po prostu nieco bardziej zwięzłe. To, co opisałeś jest funkcjonalnie identyczne i dwie dodatkowe zmienne nie mając tutaj znaczenia. Wszystko jest kwestią czytelności i przyzwyczajeń (oraz tego, jak się umówicie w zespole) – żadna z tych metod nie jest obiektywnie ‚lepsza’. Jeśli użycie dodatkowej zmiennej jest dla Ciebie czytelniejsze – w tym kontekście Twoje rozwiązanie jest lepsze :)

  • Dobry

    Objaśnienie działania bloków try-cache

    Przykład 1: dwa bloki „catch” obsługują dwa wyjątki

    -pierwszy cache: zostanie wykonany w przypadku podania złego formatu
    liczby

    -drugi cache: wykona się w przypadku wystąpienia innego wyjątku,
    niż ten opisany wyżej

    -następnie wykona się blok ”finally” (instrukcje w nim zawarte),
    ponieważ blok ten wykonuje się zawsze (mimo wyjątków), jeśli
    tylko jest zadeklarowany. Można sobie to wyobrazić jako krok
    konieczny po udanej lub nieudanej akcji – np. wyłączenie płyty
    ceramicznej w kuchni po udanym lub nie udanym przyrządzeniu potrawy

    Przykład 2:
    -niezależnie czy wystąpi błąd z przekonwertowaniem String na
    Integer czy też nie – zawsze wykona się blok „finally”. Blok
    „finally” wykonuje się nawet wtedy, kiedy powyżej niego w
    którymś cache znalazła by się instrukcja return lub break.
    Jedynym sposobem, by nie wykonały się instrukcje w bloku „finally”
    jest zamknięcie JVM instrukcją System.exit(0);

    Przykład 3:
    W tym przypadku obsłużony został tylko jeden wyjątek w bloku
    „cache” , związany ze złym formatem liczby, co skutkuje
    wyświetleniem komunikatu.

    Przykład 4:
    – Ten przykład jest analogiczny do przykładu pierwszego,w którym zostały
    obsłużone dwa wyjątki w dwóch blokach „catch”, jednak blok
    „finally” nie został zadeklarowany, więc zostaną wyłącznie
    wykonane instrukcje w którymś z bloków „catch”.

  • Dobry

    Mam pytanie do mojego kodu. Czy takie podejście jak w załączonym screenie jest poprawne? Czy raczej lepszą praktyką jest minimalizowanie kodu i ograniczanie do minimum liczby zmiennych?

    https://uploads.disquscdn.com/images/49027052d998538011e80dcc14a2106343b4d32962d9f32373131ab0eebb1c3e.png

    • „Twój kod powinien być tak napisany, by nie wymagał komentarzy, aby go zrozumieć”, to chyba kwintesencja czystego kodu ;) Także jak najbardziej dobrą praktyką jest wyciąganie wartości do zmiennych o dobrze dobranych nazwach i używanie ich potem w ramach kodu. Minimalizowanie ilości linijek, to raczej zabawa (np. co potrafisz zaprogramować w 100 linijkach) niż dobra praktyka – oczywiście, jeśli np. dwa razy używasz bloku kodu, który wygląda tak samo, to powinieneś wyłączyć go do osobnej metody i ją wywoływać, a nie kopiować kod. Jeśli interesuje Cię więcej dobrych praktyk związanych z samym pisaniem kodu to polecam książkę Clean Code, a także http://refactoring.com/catalog/, gdzie znajdziesz listę rzeczy, które możesz zastosować by zrefaktować swój kod by zwiększyć jego czytelność.

      • Dobry

        Super, dzięki za odpowiedź! :)