Wyjątki

Czasami w czasie działania programu zdarzają się błędy i program kończy działanie. Zdarzyło się nam tak np. wtedy, kiedy próbowaliśmy dzielić przez 0.

Przykład

Poniżej prosty program, w którym mamy funkcję służącą do dzielenia dwóch liczb:

package pl.coderion;

public class Main {

    public static void main(String[] args) {
        Integer x1 = divide(8, 2);
        System.out.println(x1.toString());
    }

    public static Integer divide(Integer a, Integer b) {
        return a / b;
    }
}

Dopóki funkcji divide() będziemy dostarczać „dobre” argumenty – jak np. w powyższym przykładzie liczby 8 i 2 – to funkcja będzie działać prawidłowo.

Ale jeśli przekażemy tej samej funkcji drugi argument w postaci liczby równej 0, to doprowadzimy do próby wykonania działania 8 / 0, a to musi zakończyć się błędem. Próba wykonania obliczeń na takich liczbach spowoduje zatrzymanie programu i błąd:

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at pl.coderion.Main.divide(Main.java:11)
 at pl.coderion.Main.main(Main.java:6)

ArithmeticException mówi nam o tym, że przydarzył nam się błąd i był to błąd związany z obliczeniami. A tak dokładnie ArithmeticException to nie tyle błąd co wyjątek.

Wyjątek a błąd

Mimo, że w potocznym języku często zamiennie używa się nazw błąd wyjątek, to jednak trzeba pamiętać, że to co się zdarzyło, to tak zwany wyjątek. Powstał on oczywiście w wyniku jakiejś błędnej sytuacji. W powyższym przypadku tym błędem okazała się próba dzielenia przez zero.

Błąd ponadto często też kojarzy nam się z sytuacją nieprzewidzianą, czasem zawionioną przez użytkownika, czasem przez sam program, z którą niewiele da się zrobić.

Wyjątki mają jednak to do siebie, że ich znaczenie jest dość dobrze określone i przede wszystkim da się przewidzieć wcześniej ich wystąpienie. Przecież, to że wystąpi problem z dzieleniem przez zero da się wcześniej przewidzieć, jeśli przewidzimy, że użytkownik może użyć takiej liczby w programie. A skoro da się taką sytuację przewidzieć, to również możemy się przed nią obronić.

Try… catch…

Do obsługi wyjątków posłuży nam blok try… catch… – program spróbuje wykonać kawałek kodu zawarty w bloku try, a jeśli podczas jego wykonywania zdarzy się wyjątek opisany w bloku catch, to program nie zakończy nagle swojego działania, tylko wykonają się instrukcje wewn. bloku catch. Dzięki temu będziemy w stanie dać użytkownikowi jasny i czytelny komunikat, na czym polegał problem i co poszło nie tak.

package pl.coderion;

public class Main {

    public static void main(String[] args) {
        try {
            Integer x1 = divide(8, 0);
            System.out.println(x1.toString());
        } catch (ArithmeticException e) {
            System.out.println("Wystąpił błąd podczas obliczeń. Możliwe, że próbujesz dzielić przez 0.");
        }
    }

    public static Integer divide(Integer a, Integer b) {
        return a / b;
    }
}

Teraz program nie zakończy działania błędem, tylko pojawi się czytelny komunikat:

Wystąpił błąd podczas obliczeń. Możliwe, że próbujesz dzielić przez 0.

W przykładzie powyżej zadziałał taki mechanizm: program próbował wykonać instrukcję

Integer x1 = divide(8, 0);

ale podczas tej próby wystąpił wyjątek klasy AritmeticException. Linijka kodu ze słowek catch wygląda dla przypomnienia tak:

} catch (ArithmeticException e) {

To znaczy, że spodziewamy się wystąpienia wyjątku klasy ArithemticException i jeśli rzeczywiście taki wystąpi, to wykonany powinien zostać kodu wewn. bloku catch, czyli:

System.out.println("Wystąpił błąd podczas obliczeń. Możliwe, że próbujesz dzielić przez 0.");

Inne przykładowe wyjątki

Poniżej jeszcze dwa przykłady programów, w których na pewno wystąpi wyjątek. W pierwszym z nich deklarujemy dwie zmienne typu Integer. Jednej przypisujemy wartość liczbową, druga nie ma wartości – przypisany jest null. Próba dodania obu wartości musi  zakończyć się wyjątkiem NullPointerException.

package pl.coderion;

public class Main {

    public static void main(String[] args) {

        Integer a = 1;
        Integer b = null;

        Integer suma = a + b;

        System.out.println(suma);
    }
}

I drugi przykład:

package pl.coderion;

public class Main {

    public static void main(String[] args) {

        Integer[] tablica = new Integer[] {1, 2, 3, 4, 5};

        System.out.println(tablica[4]);

        System.out.println(tablica[1000]);
    }
}

Program kończy działanie wystąpieniem wyjątku ArrayIndexOutOfBoundsException, ponieważ w ostatniej linii próbujemy dostać się do elementu tablicy o indeksie 1000 mimo, że zadeklarowana tablica ma tylko 5 elementów. Próba dostania się do elementów, których w tablicy nie ma, zawsze zakończy się takim wyjątkiem.

Przechwytywanie kilku wyjątków

Stosując kilka bloków catch możemy przechwytywać wyjątki różnych klas. Np.:

try {
  ...
} catch (NullPointerException e) {
  ...
} catch (ArithmeticException e) {
  ...
}

Wyjątki w takiej sytuacji przechwytywane będą w takiej kolejności, w jakiej wpisane zostały w blokach catch.

Hierarchia klas wyjątków

Klasy wyjątków, z którymi mieliśmy do tej pory do czynienia – np. ArithmeticExcpetion, NullPointerException, ArrayIndexOutOfBoundsException – to klasy, które łączy jedna cecha. Otóż wszystkie z nich są pochodnymi klasami klasy Exception.

Dlaczego hierarchię klas wyjątków warto znać choćby pobieżnie? Ponieważ jeśli w bloku catch przechwytywać będziemy wszystkie wyjątki klasy Exception, to nie ma już potrzeby przechwytywać wyjątków, które są klasami potomnymi klasy Exception. Przechwytując Exception nie trzeba przechwytywać np. ArithmeticException, bo jeśli taki wyjątek się zdarzy to przechwycony zostanie w bloku catch dotyczącym Exception.

Zasada jest więc taka, że wyjątki przechwytujemy w kolejności od tych najbardziej szczegółowych, najbardziej potomnych do tych, które są najwyżej w hierarchii, np. klasa Exception.

Czego nie powinno się robić

Przechwytywanie wyjątku i nie robienie niczego nie jest dobrym pomysłem. Pusty block catch jest bardzo złym pomysłem. To powoduje, że w sytuacji błędnej wyjątek zostanie przechwycony, ale programista lub użytkownik nigdy się o tym nie dowie, bo żadna akcja nie zostanie wykonana.

try {
  // pusty blok catch nie jest dobrym pomysłem!
} catch (Exception e) {
}

Jeśli nie wiemy, jak obsłużyć błędną sytuację, to w bloku catch umieśćmy przynajmniej instrukcję System.out.println(„Informacja o błędzie”), by cokolwiek pojawiło się na ekranie, co pokazałoby, że do błędu doszło.

Dobrym pomysłem jest wykorzystanie metody printStackTrace():

try {
  ...
} catch (Exception e) {
  e.printStackTrace();
}

Oprócz samego błędu będą widoczne szczegóły. W której klasie w którym miejscu wystąpił błąd, w wyniku wywołania której metody itp. Dzięki temu łatwiej będzie zlokalizować błąd i go naprawić.