Kurs Java

Wyjątki w Javie

Tak jak w naszym życiu, tak samo w kodzie programu zdarzają się sytuacje wyjątkowe, które trzeba jakoś rozwiązać. Czasami przewidujemy, że mogą one wystąpić, a czasem po prostu stajemy przed faktem dokonanym i możemy się tylko zastanawiać, co poszło nie tak. Doskonałym przykładem, w którym dochodzi do wystąpienia wyjątku, jest poniższy fragment kodu:
 1  public class MathStuff {
 2   
 3      public static void main(String[] args) {
 4
 5          MathStuff mathStuff = new MathStuff();
 6          int result = mathStuff.divide(6, 2);
 7          System.out.println(result);
 8          result = mathStuff.divide(6, 1);
 9          System.out.println(result);
10          result = mathStuff.divide(6, 0);
11          System.out.println(result);
12      }
13
14      private int divide(int firstNumber, int secondNumber) {
15          return firstNumber/secondNumber;
16      }
17  }
Wynikiem wykonania kodu będzie wydruk:
Java ArithmeticException
Wartość 3 wynika z dzielenia 6 i 2, a wartość 6 to efekt dzielenia 6 i 1. Kiedy dochodzi do dzielenia 6 i 0, program zwraca nam wyjątek o swojsko brzmiącej nazwie ArithmeticException. To, co widzimy dalej, to jest tak zwany stacktrace wyjątku, czyli ścieżka wywołań kodu, która finalnie doprowadziła do wystąpienia wyjątku. Czytamy go od ostatniej linii do pierwszej. Najpierw zauważamy, że problem dotyczy linii 10, gdzie wywoływana jest metoda divide. Następnie widzimy, że kolejnym etapem jest wywołanie linii 15 i to miejsce właśnie doprowadza do wystąpienia wyjątku.

ArithmeticException

W ten sposób dosyć dokładnie jesteśmy w stanie określić, co poszło nie tak. Wiadomo, że nie wolno jest dzielić przez 0, a że Java jest przygotowana na taką sytuację, to otrzymujemy wyjątek. Wyjątek ten jest obiektem stworzonym na podstawie klasy ArithmeticException z pakietu java.lang. Jest to jeden z wielu wyjątków stworzonych przez programistów Javy. Każdy z nich ma swoje konkretne przeznaczenie.

NullPointerException

Jednym z tak przygotowanych wyjątków, jest legendarny NullPointerException. Kiedy on występuje? Zobaczmy na przykładzie:
 1  public class Start {
 2
 3      public static void main(String[] args) {
 4
 5          Item item = new Item();
 6          item.setName("TestItem");
 7          System.out.println(item.getName().length());
 8
 9          Item item2 = new Item();
10          System.out.println(item2.getName().length());
11      }
12  }
Item to prosta klasa z jednym polem i metodami ustawiającymi/pobierającymi wartość:
public class Item {

    String name;

    void setName(String name) {
        this.name = name;
    }

    String getName() {
        return this.name;
    }
}  
Wykonując kod, najpierw zobaczymy na wydruku liczbę 8, która jest długością ciągu tekstowego "TestItem" zwróconą w linii 7. Tutaj wszystko jest w porządku, ponieważ w obiekcie item ustawiliśmy wcześniej nazwę w postaci wspomnianego ciągu tekstowego.

Problemem jest linia 10, w której drukujemy długość nazwy pobranej z obiektu item2. W tym obiekcie nazwa nie została ustawiona, więc pole name nie jest zainicjowane referencją do konkretnego obiektu tekstowego. Pole posiada domyślnie postać null. Skoro pole typu String jest nullem (tak mówimy potocznie), to próba wywołania na nim metody length kończy się wyrzuceniem przez program wyjątku NullPointerException:
NullPointerException in Java

Checked vs unchecked

Przedstawione do tej pory wyjątki są wyjątkami typu unchecked. Oznacza to, że nie spodziewamy się ich wystąpienia i szczerze mówiąc ich pojawienie się, najczęściej świadczy o błędzie programisty. Powinniśmy tak dewelopować kod, by takie wyjątki się nie pojawiały (pomijamy tutaj celowe tworzenie własnych wyjątków - o tym kiedy indziej).

W przypadku ArithmeticException możemy sprawdzić, czy wartość, przez którą dzielimy, nie jest zerem i wtedy zwrócić komunikat o niepoprawnej wartości. Jeśli chodzi o NullPointerException, wystarczy upewnić się, że nasze pola i zmienne posiadają referencje do obiektów, zanim dojdzie do wywoływania na nich konkretnych metod. Wystąpienie tego wyjątku jest dużym błędem programisty.

Oprócz wyjątków typu unchecked, w Javie występują również wyjątki typu checked, które mogą, ale nie muszą wystąpić i co najważniejsze, nie zawsze jesteśmy w stanie ich uniknąć. Przyjrzyjmy się teraz fragmentowi kodu:
Java CheckedException
Kompilator informuje nas o tym, że metoda, której chcemy użyć, może zakończyć się wyjątkiem typu IOException, a my w żaden sposób go nie obsługujemy (Unhandled exception). Co w takiej sytuacji należy zrobić, aby program mógł się skompilować? Przede wszystkim należy się zastanowić, co oznacza ten wyjątek, a następnie podjąć odpowiednie kroki w celu jego obsłużenia.

To, co próbujemy osiągnąć w naszym przykładzie, to odczyt pliku, pobranie jego zawartości i wyświetlenie danych na konsoli. Plik nazywa się items.txt i powinien znajdować się w głównym katalogu projektu. Wyjątek IOException pojawia się wtedy, gdy wystąpił problem w komunikacji z zasobem systemu, w tym przypadku z naszym plikiem. Tak więc, jeśli plik nie istnieje, albo program nie może go otworzyć, wówczas metoda get rzuci wyjątkiem.

Należy pamiętać, że każde wystąpienie nieobsłużonego wyjątku (checked albo unchecked) powoduje zatrzymanie programu, przez co nie wykona się już żadna instrukcja, która następuje po linii, z której wyszedł wyjątek. W naszym przypadku nie wykona się linia 10. Nic nie zostanie tutaj wydrukowane na konsolę.

Try/catch i try/catch/finally

Przewidując czarny scenariusz (a przede wszystkim chcąc doprowadzić do kompilacji), przygotowujemy się do obsłużenia wyjątku. Wprowadzamy klauzulę try/catch:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ItemFileReader {
    
    public static void main(String[] args) {
        
        ItemFileReader fileReader = new ItemFileReader();
        String content = fileReader.getContent("items.txt");
        System.out.println(content);        
    }
    
    private String getContent(String fileName) {
        try {
            return new String(Files.readAllBytes(Paths.get("items.txt")));
        } catch (IOException e) {
            return "No content available";
        }       
    }
}
Zastosowanie tej klauzuli polega na wykonaniu bloku try, a jeśli podczas wykonania tego kodu wystąpi wyjątek IOException, wówczas wykonany zostanie kod w bloku catch. Bloków typu catch może być więcej i będą one sprawdzenie kolejno, co pozwala na obsługę różnych typów wyjątków i zareagowanie w określony sposób na każdy z nich.

Teraz możemy zrobić coś jeszcze. Wprowadzamy dodatkową klauzulę finally:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ItemFileReader {
    
    public static void main(String[] args) {
        
        ItemFileReader fileReader = new ItemFileReader();
        String content = fileReader.getContent("items.txt");
        System.out.println(content);        
    }
    
    private String getContent(String fileName) {
        try {
            return new String(Files.readAllBytes(Paths.get("items.txt")));
        } catch (IOException e) {
            return "No content available";
        }       
        finally {
            System.out.println("Reading process completed");
        }
    }
}
Klauzula reprezentuje blok kodu, który - w przeciwieństwie do catch - zostanie wykonany zawsze (niezależnie od tego, czy wyjątek się pojawi, czy też nie). Tak więc kod w naszym przykładzie zawsze pokaże na konsoli tekst "Reading process completed". Nawet wykonanie komendy return nie pozwoli ominąć tego kawałka kodu. Na konsoli zobaczymy:
Java finally clause

Throws

W Javie nie musimy obsługiwać wyjątku dokładnie w miejscu, w którym jest on rzucany. Możemy przekazać go wyżej, czyli do metody wywołującej. W tym celu bieżącą metodę oznaczamy klauzulą throws:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ItemFileReader {
    
    public static void main(String[] args) {
        
        ItemFileReader fileReader = new ItemFileReader();   
        String content = "";
        try {
            content = fileReader.getContent("items.txt");
        } catch (IOException e) {
            content = "No content available";
        }       
        finally {
            System.out.println("Reading process completed");
        }       
        
        System.out.println(content);        
    }
    
    private String getContent(String fileName) throws IOException {
        return new String(Files.readAllBytes(Paths.get("items.txt")));
    }
}
Na powyższym snippecie widzimy, że metoda getContent została opatrzona klauzulą throws z nazwą typu wyjątku, który może zostać wyrzucony z kodu metody. W ten sposób przekazujemy potencjalny wyjątek do miejsca, w którym metoda jest uruchomiona i tam możemy obsłużyć jego wystąpienie. Możemy też w takiej metodzie (w tym przypadku main) ponownie zastosować klauzule, aby przekazać wyjątek jeszcze wyżej.

Exception i RuntimeException

Na koniec zostawiliśmy bardzo ważny aspekt dotyczący używania wyjątków w Javie. Otóż to czy wyjątek jest oznaczony, czy nie (checked albo unchecked), wynika bezpośrednio z tego, jakiego typu jest obiekt naszego wyjątku. W celu lepszego zrozumienia zerknijmy na fragment schematu klas wyjątków:
Java - Exceptions
Każdy wyjątek, który dziedziczy z hierarchii klas wywodzącej się z RuntimeException to wyjątek typu unchecked, tak więc nie będzie on oznaczony jawnie w kodzie. Natomiast wyjątki wywodzące się w hierarchii z klasy Throwable i Exception są wyjątkami typu checked i dlatego też będą wymagały zastosowania try/catch lub throws. W przypadku klasy Error też mówimy, że jest to byt nieoznaczony unchecked, natomiast nie nazywamy go wyjątkiem. Przykładem może być bardzo poważny błąd OutOfMemoryError, oznaczający brak wolnej pamięci wymaganej do wykonywania programu.


Temat tego rozdziału jest bardzo rozbudowany, choćby z tego powodu, że możemy też tworzyć własne wyjątki, a także możemy wykorzystywać hierarchię wyjątków, w celu ustalenia określonej kolejności ich obsługi. W przyszłości pewnie będziemy jeszcze wracać do tego obszaru, a na tę chwilę polecamy użyć kod z przykładów, tak by oswoić się z opisanymi tutaj zagadnieniami.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
kursjava@javappa.com
Wasze pytania
Dlaczego na wydruku z finally najpierw jest "Reading process completed", a później no content available, skoro najpierw wykonane zostaje "No content available"?

Odpowiadamy: "No content available" jest zwracane, ale jeszcze nie jest drukowane. Najpierw wykonuje się sekcja finally, metoda kończy się, a dopiero wtedy następuje wydruk zwróconej zawartości, za pomocą System.out.println(content);

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE