Rozdział 11 - Wyjątki - Korzystanie z try..catch..finally

Mechanizm łapania wyjątków ma następującą składnię:

try {
  // instrukcje ktore moga potencjalnie zakonczyc sie wyjatkiem
} catch (TypWyjatku dowolnaNazwa) {
  // instrukcje, gdy zajdzie wyjątek TypWyjatku
} catch (KolejnyTypWyjatku dowolnaNazwa2) {
  // instrukcje, gdy zajdzie wyjątek KolejnyTypWyjatku
} finally {
  // instrukcje, ktore maja byc wykonane niezaleznie od tego,
  // czy wyjatek zostal zlapany, czy nie
}

Jak widzimy, możemy zdefiniować obsłużenie większej liczby wyjątków poprzez dopisywanie kolejnych bloków catch – może ich być dowolnie wiele. W nawiasach dla każdego z bloków catch podajemy, jaki typ wyjątku chcemy obsłużyć oraz nadajemy mu nazwę, ponieważ sam wyjątek jest obiektem (zmienną), którą możemy odpytać o informacje związane z zaistniałym błędem. Dla przykładu, możemy wyświetlić komunikat błędu:

fragment pliku ZwrocWynikDzieleniaWyjatek.java
try {
  System.out.println(podziel(10, 0));
} catch (ArithmeticException e) {
  System.out.println("Nie wolno dzielic przez 0!");
  System.out.println("Wystapil blad: " + e.getMessage());
}

W wyniku czego zobaczymy na ekranie:

Nie wolno dzielic przez 0! Wystapil blad: / by zero

Fragment wypisanego tekstu "/ by zero" to opis błędu, który wystąpił. Opis ten przechowywany jest w obiekcie-wyjątku, który nazwaliśmy e. Metoda getMessage ten opis zwraca.

Mechanizm łapania wyjątków oferuje jeszcze jedną funkcjonalność – możemy w opcjonalnym bloku finally umieścić instrukcje, które mają zawsze się wykonać, niezależnie od tego, czy wyjątek zostanie złapany, czy nie. Blok finally zazwyczaj używany jest do czyszczenia zasobów, np. zamykania połączeń z bazą danych czy zamykania otwartych plików.

Istotne jest zrozumienie kolejności wykonywania instrukcji w blokach try..catch..finally:

  1. Najpierw wywoływane są instrukcje w bloku try. Jeżeli któraś z instrukcji spowoduje rzucenie wyjątku, to:
    1. Wykonanie bloku try zostaje przerwane – wszystkie instrukcje następujące po instrukcji, która spowodowała wyjątek, nie zostaną wykonane.
    2. Typ rzuconego wyjątku jest dopasowywany do wyjątków zdefiniowanych w sekcjach catch. Zostają wykonane instrukcje przyporządkowane do pierwszej sekcji catch, do której rzucony wyjątek został dopasowany. Jeżeli wyjątek, który wystąpił, nie jest obsługiwany w żadnym z bloków catch, to przechodzimy do kroku 2.
  2. Wywoływane są instrukcje z bloku finally, jeżeli blok ten jest obecny, niezależnie od tego, czy wyjątek wystąpił, czy nie.

Spójrzmy na dwa poniższe przykłady:

fragment pliku ZwrocWynikDzieleniaWyjatek.java
try {
  System.out.println("Zaraz podzielimy 10 przez 2");
  System.out.println("Wynik dzielenia: " + podziel(10, 2));
  System.out.println("Podzielilismy 10 przez 2");
} catch (ArithmeticException e) {
  System.out.println("Wystapil blad podczas dzielenia przez 2!");
} finally {
  System.out.println("Blok try..catch..finally zakonczony!");
}

Ten przykład spowoduje wypisanie na ekran:

Zaraz podzielimy 10 przez 2 Wynik dzielenia: 5 Podzielilismy 10 przez 2 Blok try..catch..finally zakonczony!

Jak widzimy, komunikat z sekcji catch nie został wypisany, ponieważ żaden wyjątek nie wystąpił.

Spójrzmy na drugi, ciekawszy przykład:

fragment pliku ZwrocWynikDzieleniaWyjatek.java
try {
  System.out.println("Zaraz podzielimy 10 przez ZERO!");
  System.out.println("Wynik dzielenia: " + podziel(10, 0));
  System.out.println("Podzielilismy 10 przez ZERO!"); // 1
} catch (ArithmeticException e) {
  System.out.println("Wystapil blad podczas dzielenia przez ZERO!"); // 2
} finally {
  System.out.println("Blok try..catch..finally zakonczony!");
}

Tym razem na ekranie zobaczymy:

Zaraz podzielimy 10 przez ZERO! Wystapil blad podczas dzielenia przez ZERO! Blok try..catch..finally zakonczony!

Tym razem nie została wykonana instrukcja (1), która następowała po instrukcji dzielenia. Nie zobaczyliśmy na ekranie napisu "Podzielilismy 10 przez ZERO!", ponieważ w momencie rzucenia wyjątku wykonanie programu przeszło od razu do obsługi wyjątku. Z racji tego, że wyjątek wystąpił, zobaczyliśmy napis wypisywany w obsłudze wyjątku (2).

Zwróćmy uwagę, że niezależnie od tego, czy wyjątek wystąpił, czy nie, w obu przykładach wykonany został kod z części finally.

Kod z bloku finally nie zostanie wykonany w szczególnym przypadku – gdy użyjemy metody exit z klasy System, ponieważ natychamiast kończy ona nasz program.

Zakres zmiennych definiowanych w bloku try

Wielokrotnie przy okazji omawiania instrukcji warunkowych, pętli, metod itp. widzieliśmy, że zmienne definiowane wewnątrz bloków kodu są niewidoczne poza tymi blokami, jeżeli nie wskazuje na nie żadna referencja spoza tego bloku. Jeżeli zdefiniujemy zmienną w bloku try, to po zakończeniu tego bloku przestanie ona istnieć – nie będziemy mieli do niej dostępu nawet w sekcjach catch i finally – spójrzmy na przykład:

fragment metody main z pliku ZwrocWynikDzieleniaWyjatek.java
try {
  int wynik = podziel(10, 2);
} catch (ArithmeticException e) {
  System.out.println("Blad dzielenia, zmienna wynik ma wartosc: " + wynik);
} finally {
  System.out.println("Sekcja finally: wynik wynosi " + wynik);
}

Ten fragment kodu powoduje następujące błędy kompilacji:

ZwrocWynikDzieleniaWyjatek.java:36: error: cannot find symbol System.out.println("Blad dzielenia, zmienna wynik ma wartosc: " + wynik); ^ symbol: variable wynik location: class ZwrocWynikDzieleniaWyjatek ZwrocWynikDzieleniaWyjatek.java:38: error: cannot find symbol System.out.println("Sekcja finally: wynik wynosi " + wynik); ^ symbol: variable wynik location: class ZwrocWynikDzieleniaWyjatek 2 errors

W sekcjach catch i finally próbujemy odnieść się do nieistniejącej zmiennej – zmienna wynik istnieje jedynie w bloku try, bo w nim została zdefiniowana.

W praktyce często zachodzi potrzeba korzystania "na zewnątrz" sekcji try z tworzonych w niej zmiennych, czy też w sekcji finally. W takich przypadkach należy zdefiniować zmienną przed sekcją try:

fragment metody main z pliku ZwrocWynikDzieleniaWyjatek.java
int wynik = 0;

try {
  wynik = podziel(10, 2);
} catch (ArithmeticException e) {
  System.out.println("Blad dzielenia, zmienna wynik ma wartosc: " + wynik);
} finally {
  System.out.println("Sekcja finally: wynik wynosi " + wynik);
}

System.out.println("Po try wynik wynosi " + wynik);

Wynik działania:

Sekcja finally: wynik wynosi 5 Po try wynik wynosi 5

Tym razem kod skompilował i wykonał się bez problemów.

Zauważmy jeszcze jedną istotną cechę powyższego kodu – zmienna wynik została zainicjalizowana wartością 0. Jeżeli byśmy tego nie zrobili, to kod ponownie by się nie skompilował. Kompilator zgłosiłby błąd variable wynik might not have been initialized. Powodem błędu byłby fakt, że metoda podziel może rzucić wyjątek i nie zwrócić żadnej wartości – w takim przypadku zmiennej wynik nie zostałaby przypisana żadna wartość, a jak wiemy z rozdziału o zmiennych – zmienne lokalne muszą być zainicjalizowane wartością przed użyciem. Dlatego przed blokiem try, w linii, w której definiujemy zmienną wynik, nadajemy jej od razu wstępną wartość. Jeżeli korzystalibyśmy z zmiennej typu złożonego, to jako wartość domyślną moglibyśmy przypisać np. null.

Metoda getInt i obsługa wyjątków

W poprzednich rozdziałach wielokrotnie korzystaliśmy z metody getInt, która miała za zadanie zwrócić pobraną od użytkownika liczbę. Co się jednak działo w sytuacjach, gdy przez przypadek podaliśmy nieprawidłową liczbę?

import java.util.Scanner;

public class GetIntObslugaWyjatkow {
  public static void main(String[] args) {
    System.out.print("Podaj liczbe: ");
    int x = getInt();

    int kwadrat = x * x;
    System.out.println("Kwadrat tej liczby wynosi " + kwadrat);
  }

  public static int getInt() {
    return new Scanner(System.in).nextInt();
  }
}

Jeżeli po uruchomieniu powyższego programu podamy np. 'x' jako liczbę, to program zakończy się następującym błędem:

Podaj liczbe: x Exception in thread "main" java.util.InputMismatchException at java.base/java.util.Scanner.throwFor(Scanner.java:939) at java.base/java.util.Scanner.next(Scanner.java:1594) at java.base/java.util.Scanner.nextInt(Scanner.java:2258) at java.base/java.util.Scanner.nextInt(Scanner.java:2212) at GetIntObslugaWyjatkow.getInt(GetIntObslugaWyjatkow.java:13) at GetIntObslugaWyjatkow.main(GetIntObslugaWyjatkow.java:6)

Teraz już wiemy, że ten błąd to rzucony wyjątek. W tym przypadku, nazywa się on InputMismatchException, które zdefiniowany jest w bibliotece standardowej w pakiecie java.util.

Wiedząc już, jak obsługiwać wyjątki, możemy zmodyfikować nasz program, by brał pod uwagę możliwość podania przez użytkownika nieprawidłowej liczby. Jak jednak powinniśmy taką sytuację obsłużyć?

Możemy w pętli próbować pobrać od użytkownika liczbę – jeżeli wyjątek zostanie rzucony, poinformujemy użytkownika o nieprawidłowo podanej wartości i spróbujemy pobrać ją kolejny raz w następnym obiegu pętli. Gdy użytkownik poda poprawną liczbę, zmienimy zmienną warunkującą wykonanie pętli, by pętla już więcej się nie wykonywała.

Spójrzmy, jak mogłaby wyglądać implementacja:

Nazwa pliku: GetIntObslugaWyjatkow.java
import java.util.InputMismatchException; // 1
import java.util.Scanner;

public class GetIntObslugaWyjatkow {
  public static void main(String[] args) {
    boolean wartoscPodana = false; // 2
    int x = 0;

    while (!wartoscPodana) { // 3
      try {
        System.out.print("Podaj liczbe: ");
        x = getInt(); // 4

        wartoscPodana = true; // 5
      } catch (InputMismatchException e) { // 6
        System.out.println("To nie jest liczba!");
      }
    }

    int kwadrat = x * x;
    System.out.println("Kwadrat tej liczby wynosi " + kwadrat);
  }

  public static int getInt() {
    return new Scanner(System.in).nextInt();
  }
}

Najpierw dodajemy importowanie typu wyjątku, który będziemy łapać (1). Wyjątek ten nie znajduje się w pakiecie java.lang, lecz java.util, dlatego musimy sami dodać jego import do naszego programu.

Następnie definiujemy w metodzie main zmienne wartoscPodana i x (2). Pierwsza będzie wskazywała, czy użytkownik podał już poprawną wartość, czy jeszcze nie. Druga zmienna przechowuje wartość od użytkownika.

W pętli while (3) wykonujemy próbę pobrania liczby od użytkownika (4) tak długo, aż nie poda on poprawnej liczby. Jeżeli użytkownik poda poprawną liczbę, to w linii (5) ustawimy wartoscPodana na true, dzięki czemu pętla nie wykona się więcej razy. Jeżeli jednak użytkownik poda np. literę zamiast liczby, to wywołanie metody getInt zakończy się rzuceniem wyjątku InputMismatchException, który obsługujemy w linii (6). W tym przypadku informujemy użytkownika, że podał nieprawidłową liczbę. Instrukcja try..catch się kończy i przechodzimy do kolejnego obiegu pętli – w zależności od tego, jaką wartość podał użytkownik, wykona się ona ponownie lub zakończy działanie.

Na końcu programu liczymy kwadrat pobranej liczby i wypisujemy wynik.

Przykładowe wykonanie tego programu:

Podaj liczbe: x To nie jest liczba! Podaj liczbe: y To nie jest liczba! Podaj liczbe: 5 Kwadrat tej liczby wynosi 25

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.