Kurs Java od podstaw Tydzień 4

Wyjątki

Wyjątki

Kilka razy mówiłem, że dojdziemy do wyjątków i w końcu nadeszła ta lekcja. W tej lekcji dowiesz się czym są wyjątki, na jakie grupy je dzielimy, czym się różnią, jak je rzucać oraz jak tworzyć własne wyjątki.

Jak widzisz zagadnień jest sporo, więc nie przedłużajmy tylko zaczynajmy!

Czym jest wyjątek?

Rozpocznijmy od powiedzenia czym jest wyjątek – exception. Jak sama nazwa wskazuje jest to coś wyjątkowe 😉 – i w sumie tak jest. Wyjątki są rzucane – tak się mówi bo używa się komendy throw – właśnie w wyjątkowych sytuacjach.

Mówiąc wyjątkowe sytuacje – mam na myśli trochę takie bardziej krytyczne sytuacje, niż pozytywne.

Wniosek jest prosty – wyjątek może być rzucony tak naprawdę zawsze, jednak robi się to, gdy coś pójdzie nie do końca po naszej myśli.

Kiedy się rzuca wyjątki?

Wspomniałem, że można je rzucać przy pomocy słowa kluczowe throw – ale kiedy je dokładnie rzucać?

Przypuśćmy, że chcemy znaleźć Usera po jego id jednak mamy takiego pecha, że nie istnieje. Co możemy zrobić w takiej sytuacji.

Możemy np. zwrócić null tak jak to robiliśmy do tej pory co wiązało się z koniecznością sprawdzenia czy User, który przyszedł z serwisu nie jestem nullem. Trochę słabe rozwiązanie, możemy jednak wykorzystać wyjątki.

Jest to taka trochę negatywna sytuacja – w końcu coś sie nie udało, dlatego możemy rzucić wyjątkiem stworzonym przez siebie.

Takie wyjątek może się np. nazywać UserNotFoundException – ładna prosta nazwa opisująca zadanie wyjątku.

Kiedy są rzucane wyjątki?

To nie jest tak, że wyjątki tylko my rzucamy – rzuca je każdy, nawet biblioteki Javy. W końcu już się spotkałeś z wyjątkami podczas pisania takiego kodu w klasie DAO:

public void saveProducts(List<Product> products) throws FileNotFoundException {
    FileUtils.clearFile(fileName);
    PrintWriter printWriter = new PrintWriter(new FileOutputStream(fileName, true));
    for(Product product : products) {
        printWriter.write(product.toString() + "\n");
    }
    printWriter.close();
}

W tym przypadku klasa FileOutputStream wymusza na nas zajęcie się wyjątkiem – można to zrobić na dwa sposobem, które zaraz Ci wytłumaczę. Jednym z nim jest właśnie dopisanie do metody throws [nazwa-wyjątku]. 

Wyjątek ładnie się nazywa FileNotFoundException czyli znowu opisuję, że coś poszło nie tak – czyli w tym przypadku obiekt nie mógł znaleźc podanego w konstruktorze pliku. Logiczne, w końcu tak może się stać – i dodatkowo jest to dobre rozwiązanie, ponieważ od razu programista wie co się stało łapiąc wyjątek – czyli to będzie drugi sposób. Ale o tym za chwilę.

Checked vs Unchecked & Exception vs RuntimeException

Wspominałem o podziale wyjątków na gurpy – dzielimy na checked unchecked. Czym one się różnią?

Wyjątki checked to takie wyjątki, które muszą być obsłużone w kodzie – przykładem takiego wyjątku jest właśnie powyższy FileNotFoundException. Wymusza on na nas obsługę, w przeciwnym wypadku kod się nie skompiluje.

Mamy jeszcze wyjątki unchecked – jak się domyślasz to takie, których nie trzeba łapać – ale można! Przykładem takiego wyjątku, z którym się spotkałeś podczas przerabiania tego kursu jest NullPointerException – jest to wyjątek, które jest rzucany, gdy próbujemy wykonać coś na obiekcie, który jest nullem.

Wszystkie wyjątki, które istnieją muszą dziedziczyć po Exception – wyjątki, które dziedziczą bezpośrednio po niej są wyjątkami checked czyli wymaganymi do obsłużenia.

Są jeszcze wyjątki, które dziedziczą po RunTimeException (a ta klasa tak naprawdę dziedziczy też po Exception)  – i to sa wyjątki z rodziny unchecked – czyli takie, których nie musimy obsługiwać, jednak dadzą nam o sobie znać podczas działania programu.

Stąd ta nazwa RunTime – czyli podczas dzialania aplikacji. 😉

Obsługa wyjątków

Skoro już wiesz o co chodzi w tych wyjątkach to przejdźmy do praktyki. Pokażę Ci teraz dwa sposoby obsługi wyjątków – w jednym będziemy wypychać wyjątek w górę, w drugim będziemy go łapać. Oczywiście te dwa sposoby można łączyć, aby otrzymać oczekiwany rezultat. 😉

Throws exception

Jeżeli jakaś metoda – konstruktor – wymaga od nas obsługę wyjątków to możemy go „wypchnąć wyżej” przy użyciu słowa kluczowe throws.

Tak samo jak robiliśmy to w metodzie saveProducts z klasy ProductService:

public void saveProducts(List<Product> products) throws FileNotFoundException {
    FileUtils.clearFile(fileName);
    PrintWriter printWriter = new PrintWriter(new FileOutputStream(fileName, true));
    for(Product product : products) {
        printWriter.write(product.toString() + "\n");
    }
    printWriter.close();
}

Dzięki temu w kolejnej metodzie, gdzie wywołamy saveProducts będziemy musieli znowu obslużyć ten wyjątek. Dzieje się tak, ponieważ został on wypchany wyżej, zrzucliliśmy odpowiedzialność na kolejną metodę. Na tą, która będzie wywoływała metodę saveProducts.

A wystarczy to zrobić do sygnatury metody dodać:

throws [nazwa-wyjatku]

Czyli np.:

public void saveUsers(List<User> users) throws FileNotFoundException {

Skoro tak wypychamy ten wyjątek – choć możemy go wypychać, aż do metody main i nigdzie go tak naprawdę nie złapać, lecz nie na tym zależy. Chcemy tak naprawdę go złapać i jakoś zareagować. 😉

Łapanie wyjątku

Skoro z jakieś metody został wypchany wyjątek lub po prostu jakaś metoda go rzuca nam to możemy go też złapać, a nie tylko wypychać w nieskończoność.

Do tego służy tzw. blok try – catch. Choc do chodzienia do niego jeszcze jedna sekcja – czyli finally. Już pokazuję na suchym przykładzie o co chodzi. 😉

Przypuśćmy, że chcemy dopisać coś do pliku – dlatego potrzebujemy obiektu PrintWriter i FileOutputStream

PrintWriter printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));

Jak widać w IntelliJ coś się już świeci na czerwono – pokazuje on nam, że musimy obsłużyć wyjątek.

Dlatego tworzymy blok try, w którym umieszamy tworzenie obiektu printWriter

try {
    PrintWriter printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));
}

Jak sama nazwa try wskazuje – próbujemy coś zrobić.

Jednak blok try nie jest kompletny – a by był kompletny potrzebujemy bloku catch – który odpowiada, za złapanie wyjątku.

try {
     PrintWriter printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));
} catch(FileNotFoundException e) {

    
}

W nawiasach przy catch umieszczany nazwę wyjątku – czyli tak naprawdę klasy oraz jakąś nazwę obiektu – bo podczas łapania wyjątku tak naprawdę łapiemy obiekt, który ma coś wspólnego z klasa Exception. 😉

Nazwa obiektu to e, ponieważ programiści są leniwi – a jest dużym skrótem od exception. 

Jak widać IntelliJ już nam odpuścił i tak naprawdę kod już by się skompilował. Jednak takie łapanie wyjątków jest bardzo złą praktyką – jest to ukrywanie informacji. Skoro łapiemy już wyjątek to dodajmy o tym informację – wyświetlmy np. błąd przy użyciu metody printStackTrace() – czyli wszystkie błędy o wyjątku.

Dopiszmy jeszcze do naszego kodu zapisywanie tekstu do pliku.

try {
    PrintWriter printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));
    printWriter.append("Exception master here");
} catch(FileNotFoundException e) {
    e.printStackTrace();
}

Czyli mamy taki kod – uruchommy program.

No i w konsoli nie otrzymuje nic, ponieważ wyjątek nie zostały rzucony choć plik nie istniał. W dokumentacji Javy możemy przeczytać:

FileNotFoundException - if the file exists but is a directory rather than a regular file, does not exist but cannot be created, or cannot be opened for any other reason

Oznacza to tylko tyle, że wyjątek jest rzucany, gdy klasa nie może utworzyć pliku z powodu np. braku praw dostępu do katalogu. 😉

Skoro nie udało nam się złapać wyjątku to bardziej zasymulujmy problem i spróbujmy np. wymusić wyjątek NullPointerException – choć jest to wyjątek RunTimeException, które nie musimy łapać to i tak go złapmy. 😉

Przypuśćmy, że otrzymaliśmy nulla z jakieś metody:

String name = null;

I sprawdźmy jego długość:

System.out.println(name.length());

I oczywiście my już wiemy, że dostaniemy wyjątek NullPointerException, dlatego opakujmy to w blok try – catch.

String name = null;
try {
    System.out.println(name.length());
} catch(NullPointerException e) {
    System.out.println("null pointer exception");
}

Po uruchomieniu otrzymujemy:

null pointer exception

Jak widać weszliśmy do bloku catch. 😉

Wcześniej wspominałem jeszcze o bloku finally – wróćmy do kodu wcześniejszego:

try {
    PrintWriter printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));
    printWriter.write("Exception master here");
} catch(FileNotFoundException e) {
    e.printStackTrace();
}

To w takim przypadku możemy wykorzystać blok finally, który się wykonuje zawsze! Bez względu na to czy wyjątek został rzucony, czy nie. W takim razie zamknijmy plik w bloku finally.

finally {
            printWriter.close();
        }

Tylko wtedy definicję obiektu printWriter musimy wyrzucić poza blok try – inaczej nie będzie widoczny w bloku finally.

PrintWriter printWriter =  null;
try {
    printWriter = new PrintWriter(new FileOutputStream("jakis-plik.txt", true));
    printWriter.write("Exception master here");
} catch(FileNotFoundException e) {
    e.printStackTrace();
} finally {
    printWriter.close();
}

I teraz mamy pewność, że plik został zamknięty. 😉

Tworzenie własnych wyjątków

Skoro rozumiesz na czym polega idea wyjątków to czas na stworzenie własnych.

Własne wyjątki to tak naprawdę puste klasy, których nazwa ma symbolizować nazwę problemu, które reprezentują.

Tak jak wspomniałem wcześniej – są to puste klasy, które muszą dziedziczyć po Exception, gdy chcemy stworzyc wyjątek checked lub RunTimeException, gdy chcemy stworzyć unchecked. My stworzymy ten pierwszy.

Przypuśćmy, że chcemy stworzyć wyjątek, który będzie rzucany, gdy w tablicy nie zostanie znaleziona liczba. Nazwa wyjątku ma symbolizować problem, dlatego wybrałem nazwę NumberNotFoundException – pamiętaj o sufiksie Exception, aby było wiadomo na pierwszy rzut oka o co chodzi.

W projekcie możesz stworzyć pakiet exception, w którym będzie trzymał wszystkie swoje wyjątki.

public class NumberNotFoundException extends Exception {
    public NumberNotFoundException(String message) {
        super(message);
    }
}

Można skorzystać z konstruktora bezparametrowego, lecz ja korzystam z konstruktora parametrowego, dzięki, któremu mogę zapisać dodatkowe informację – np. o tym jakiej liczby nie znaleziono, albo id jakiego usera. 😉

Teraz mając tablicę liczb:

package pl.maniaq;


public class Main {

    public static void main(String[] args) {

        int [] numbers = {1, 5, 3, 4, 9, 0};
    }
}

Mogę stworzyć metodę do sprawdzenia czy element jest w podanej tablicy.

public static boolean isFoundNumber(int [] numbers, int element) {
    for(int number : numbers) {
        if (number == element) {
            return true;
        }
    }
    return false;
}

Jednak tą metodę możemy wzbogacić o dodanie naszego wyjątku – który odpowie na negatywny rezultat, czyli zastąpi return false.

public static boolean isFoundNumber(int [] numbers, int element) {
    for(int number : numbers) {
        if (number == element) {
            return true;
        }
    }
    throw new NumberNotFoundException(element + " not found");
}

Ze względu, że jest to wyjątek checked to musimy jeszcze wypchnąć nasz wyjątek na zewnątrz. 😉

public static boolean isFoundNumber(int [] numbers, int element) throws NumberNotFoundException {
    for(int number : numbers) {
        if (number == element) {
            return true;
        }
    }
    throw new NumberNotFoundException(element + " not found");
}

I wtedy w main musimy użyć bloku try catch

public static void main(String[] args) {

    int [] numbers = {1, 5, 3, 4, 9, 0};
    
    try {
        System.out.println("Number found: " + isFoundNumber(numbers, 10));
    } catch (NumberNotFoundException e) {
        e.printStackTrace();

Ze względu, że wpisaliśmy liczbę 10, która nie istnieje w tablicy powinniśmy otrzymać:

pl.maniaq.exception.NumberNotFoundException: 10 not found
  at pl.maniaq.Main.isFoundNumber(Main.java:14)
  at pl.maniaq.Main.main(Main.java:22)

Czyli całą informację o naszym wyjątku wraz z wiadomością, którą przekazaliśmy.

Czy warto przekazywać wiadomość? Moim zdaniem tak, pozwala to na debugowanie aplikacji i dociekanie co poszło nie tak. 😉

Podsumowanie

Podsumowując, wyjątki pozwalają nam w łatwiejszy sposób na reagowanie na sytuację, które do końca nie wykonały się prawidłowo. Potrafią też nas poinformować nas o sytuacjach krytycznych – gdy np. wyjdziemy poza zakres tablicy lub chcemy wykonywać operację na nullu.

Aby utrwalić swoją wiedzę na temat wyjątków wykonaj poniższe zadania:

  1. Stwórz tablicę {1, 5, 4, 2, 9, 10, 24, 23} i wyświetl wszystkie elementy z tablicy zaczynając od 0 do tablica.length + 1 – podczas uruchomienia programu powinieneś otrzymać wyjątek. Przeczytaj w konsoli nazwę wyjątku i przy użyciu bloku try – catch złap go, wyświetlając odpowiednią informację.
  2. Swórz klasę Human przechowującą pola id, name i lastname. Stwórz wyjątek checked HumanNotFoundException, nastepnie stwórz klasę HumanService (oraz listę Humanów), która będzie posiadała metodę getHumanByLastName oraz getHumanById. W przypadku, gdy Human nie zostanie znaleziony na liście to wyrzuć wyjątek HumanNotFoundException z odpowiednią informacją o poszukiwanym człowieku.
  3. Stwórz klasę HumanValidator, która będzie miała za zadanie sprawdzać poprawność pól Humana. Klasa ma zawierać metody statyczne odpowiedzialne za sprawdzenie czy imię jest dłuższe od 3 oraz czy nazwisko jest dłuższe od 5 (taki suchy przykład 😉 ) – w przypadku, gdy tak nie jest rzuć wyjątek HumanNameWrongFormat lub HumanLastNameWrongFormat.. W klasie HumanService stwórz metodę addHuman(String name, String lastname), w której przy użyciu metod z HumanValidator sprawdzisz poprawność danych i  w przypadku, gdy nie zostanie rzucony żaden wyjątek stworzysz nowego Humana i dodasz go do listy w klasie HumanService.

Jeśli masz jakiś problem z rozwiązanie to pisz w komentarzu, a postaram Ci się pomóc!

Przykładowe rozwiązanie zadań możesz znaleźć tutaj.;)

 

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments