Prawo Bolingsa - jeżeli czujesz się świetnie, nie martw się, to minie.
Część 3.5. Obsługa wyjątków
Wyjątki  |   try ... catch  |   throws  |   Hierarchia wyjątków

Powstawanie wyjątków

Wyjątek w programie Javy to sytuacja, gdy wykonywany program powoduje powstanie błędu, np: dzielenie przez zero, próba otwarcia nieistniejącego pliku, itp. Obsługa wyjątków pozwala programiście zachować kontrolę nad poprawnościś wykonania metody czy pojedynczej instrukcji.

W Javie istnieje bardzo rozbudowana hierarchia predefiniowanych klas wyjątków, których klasą bazową jest klasa Throwable, a główne podklasy to: Error i Exception. Niektóre wyjątki należą do grupy tzw. wyjątków weryfikowalnych: kompilator sprawdza, czy program zawiera procedury obsługi dla każdego wyjątku z tej grupy, a brak takiej obsługi sygnalizowany jest komunikatem błędu podczas kompilacji. Wyjątki klasy Error i jej klas pochodnych są nieweryfikowalne, ponieważ mogą one wystąpić w wielu punktach programu i powrót z nich jest bardzo skomplikowany lub niemożliwy. Do wyjątków nieweryfikowalnych należą także wyjątki klasy RuntimeException (podklasa Exception), gdyż zadeklarowanie takich wyjątków nie może być kontrolowane przez kompilator: powstają podczas pracy programu ze względu na nieprzewidziany układ danych.

Do obsługi wyjątków weryfikowalnych służą słowa kluczowe: throw, throws, try, catch i finally.

Instrukcja try ... catch

Do obsługi wyjątków najczęściej korzystamy z instrukcji try-catch-finally. Blok try zawiera fragment kodu (całą metodę, jej fragment lub pojedynczą instrukcję), w którym może pojawić się błąd. Definiując ten blok nie precyzujemy o obsługę jakich błędów chodzi. Określamy nim jedynie obszar, gdzie może wystąpić błąd, które chcemy obsłużyć. Jeżeli w trakcie działania programu wystąpi błąd, działanie procedur bloku try zostanie zakończone, a uruchomione zostaną procedury w odpowiadającym mu bloku catch.

Właściwa obsługa wyjątku (błędu) wykonywana jest więc przez odpowiednie instrukcje w występujących dalej blokach catch. Każdy z nich odpowiedzialny jest za obsługę dokładnie jednego rodzaju wyjątków.

Blok finally wykonywany jest zawsze: po obsłużeniu któregokolwiek z powstałych wyjątków, a także wtedy, gdy żaden wyjątek nie wystąpił. Jest on wykonywany także wtedy, gdy blok catch będzie przerywał dalsze wykonywanie bieżącej metody.

Pełna struktura instrukcji try ... catch wygląda następująco:

void Przyklad
{  try
   {
        // blok instrukcji, które mogą spowodować wystąpienie wyjątku 
   }

   catch (ObiektImplementujacyInterfejsThrowable zmienna)
   {
        // blok obsługujący wystąpienie wyjątku;
        // wykonywany tylko, gdy wystąpi wyjątek typu takiego jak 
        // typ parametrem bloku catch
   }
   
   catch (ObiektImplementujacyInterfejsThrowable zmienna)
   {
        // .....
   }

     // .....

   finally         // opcjonalnie
   {
        // zakończenie działania metody;
        // instrukcje tu zapisane będą wykonane zawsze:
        // nawet po instrukcji return lub wystąpieniu wyjątku
   }
}

Żeby lepiej zrozumieć obsługę wyjątków w Javie napiszemy program, który będzie generował sytuacje błędne. Program będzie tylko wyświetlał komunikaty o wystąpieniu lub nie wystąpieniu błędu. W przypadku wystąpienia błędu będziemy wyświetlać dodatkowo komunikat o typie błędu i dodatkowe informacje generowane przez maszynę wirtualną Javy.

public class Przyklad
{ public static void main (String[] args)
    // pięciokrotne wykonanie pętli
  { for (int i = 0; i <= 4; i++) 
    { // tutaj zaczyna się blok try, który może wygenerować błąd
	  try
      { wykonaj(i);
        // następna instrukcja wykona się tylko, jeżeli błąd nie wystąpi
        System.out.println("Wyjatek nr " + i + " nie zostal wygenerowany");
      }
      // koniec bloku try

      catch (Exception e)   // blok wykonywany, gdy wystąpi błąd
      { System.out.println("Operacja nr " + i + ": wyjatek: " + e.getClass()
           + "\n      z informacja: " + e.getMessage());
      }
    }
  }

  static int wykonaj(int i)
  { 
    // tutaj zaczyna się blok try, który może wygenerować błąd
    try
    { if (i == 1) 
      { int x = 0;
        return x / x;                       // dzielenie przez 0
      }
      if (i == 2) 
      { String s = null;
        return s.length();                  // długość pustej zmiennej
      }
      if (i ==3 )
      { int t[] = new int[10];
        return t[10];                       // modyfikator poza tablicą
      }
      return 0;
    } 
    // koniec bloku try

    // blok finally, który będzie wykonany zawsze !!!
    finally
    { System.out.println("\n[generowanie wyjatku nr " + i +" zakonczone]");
    }
  }
}

Po uruchomieniu programu powinieneś otrzymać taki efekt:

Jak działa ten program? Z metody main wywoływana jest pięciokrotnie metoda wykonaj, za każdym razem z inna wartością parametru i. Zwracana z metody wykonaj wartość zależy od podanego parametru. Prześledźmy więc kolejno co będzie się działo w programie dla użytych wartości parametru i:

  • i = 0 - metoda wykonaj nie realizuje żadnych działań poza return 0;. Mimo to pojawia się komunikat o zakończeniu generowania wyjątku 0: blok finally - jak już wiesz - wykonywany jest zawsze po bloku try, nawet gdy nie ma błędu i napotkano wcześniej instrukcją return. Ponieważ nie wystąpił błąd, to wyświetla się komunikat: Wyjatek nr 0 nie został wygenerowany.
     
  • i = 1 - następuje dzielenie przez zero: metoda wykonaj wyświetla komunikat o zakończeniu generowania błędu nr 1. Ponieważ wystąpił wyjątek (błąd), to metoda main pomija pozostałe instrukcje bloku try i przechodzi do następującego po nich bloku catch. W bloku tym wyświetlany jest komunikat o wystąpieniu wyjątku, nazwa klasy wyjątku oraz dodatkowe informacje dostarczane przez maszynę wirtualną Javy.
    Gdyby nie było bloku try ... catch, program zostałby w tym miejscu przerwany. Ponieważ jednak obsłużyliśmy napotkany wyjątek, to program jest kontynuowany.
     
  • i = 2 - metoda wyjątek usiłuje zwrócić długość pustego łańcucha znakowego (przypisanie wartości null do zmiennej łańcuchowej). Ponieważ nie istniej długość pustego łańcucha znaków, to dalsze działanie programu jest identyczne, jak dla i = 1.
     
  • i = 3 - metoda wyjątek usiłuje zwrócić nieistniejący element tablicy (pamiętasz chyba, że dla tablicy o 10 elementach możemy używać tylko modyfikatorów od 0 do 9). Dalsze działanie programu będzie identyczne, jak dla i = 1.
     
  • i = 4 - metoda wykonaj nie realizuje żadnych działń poza return 0;. Ponieważ nie wystąpił błąd, to dalsze działanie programu jest identyczne, jak dla i =0.
     

W poprzednim przykładzie wystąpił tylko jeden blok catch. Wykonywany był on dla każdego typu błędu. Nie zawsze taka sytuacja jest wygodna. Nieraz możemy chcieć wykonać różne działania, zależne od typu występującego wyjątku. W tym celu możemy stworzyć wiele bloków catch. Skorzystamy z tego, że po wystąpieniu wyjątku w bloku try, Java porównuje wyjątek, który wystąpił z parametrami poszczególnych bloków catchi wykonuje tylko ten blok, którego parametr jest zgodny z typem występującego wyjątku. W ten sposób możemy obsłużyć wystąpienie wyjątków różnego rodzaju.

Spróbujmy skorzystać z tej właściwości, wykorzystując ją w naszym poprzednim programie. W tym celu przerobimy go, aby wyglądał tak:

public class Przyklad
{ public static void main (String[] args)
  { String s = null;
    int t[] = new int[10];

    for (int i = 0; i <= 4; i++) 
    { // tutaj zaczyna się blok try, który może wygenerować błąd
      try
      { if (i == 1) System.out.println("0 / 0 = " + (0 / 0));
        if (i == 2) System.out.println("Dlugosc napisu = " + s.length());
        if (i == 3) System.out.println("10 element tablicy = " + t[10]);
        // następna instrukcja wykona się tylko, jeżeli błąd nie wystapi
        System.out.println("i == " + i + ": wyjątek nie zostal wygenerowany");
      } 

      // ten blok catch wykona się tylko dla błędów arytmetycznych
      catch (ArithmeticException e)
      { System.out.println("Wystapil wyjatek: dzielenie przez zero");
      }

      // ten blok catch wykona się tylko dla wskaźnika null
      catch (NullPointerException e)
      { System.out.println("Wystapil wyjatek: operacja na wartosci null");
      }

      // ten blok catch wykona się tylko dla błędów dostępu do tablicy
      catch (ArrayIndexOutOfBoundsException e) 
      { System.out.println("Wystapil wyjatek: przekroczony zakrees tablicy");
      }
    }
  }
}

Po uruchomieniu programu powinieneś otrzymać taki efekt:

Analiza tego programu jest podobna do poprzedniego. a spróbuj przeprowadzić ją samodzielnie.

Instrukcja throws

Powróćmy do naszego pierwszego przykładu. Powstanie wyjątku w metodzie wykonaj powodowało powrót do metody main i wykonanie zapisanego tam bloku catch. Taki sam efekt możemy osiągnąć w inny jeszcze sposób: korzystając z instrukcji throws.

Instrukcja throws przekazuje - po wystąpieniu wyjątku - sterowanie do skojarzonego z nim bloku catch. Jeśli blok catch nie występuje w bieżącej metodzie, to sterowanie natychmiastowo, bez zwracania wartości, przekazywane jest do metody, która wywołała bieżącą funkcję. W tej metodzie szukany jest blok catch. Jeśli blok catch nie zostanie znaleziony, to sterowanie przechodzi do metody, która wywołała tę metodę... Sterowanie przekazywane jest zatem zgodnie z łańcuchem wywołań metod, aż do momentu znalezienia bloku catch odpowiedzialnego za obsługę wyjątku.

Z wykorzystaniem instrukcji throws nasz program wygląda tak:

public class Przyklad
{ public static void main (String[] args)
  { for (int i = 0; i <= 4; i++) 
    { // tutaj zaczyna się blok try, który może wygenerować błąd
	  try
      { wykonaj(i);
        System.out.println("Wyjatek nr " + i + " nie zostal wygenerowany");
      }
      // koniec bloku try

      catch (Exception e)   // blok wykonywany, gdy wystąpi błąd
      { System.out.println("Operacja nr " + i + ": wyjatek: " + e.getClass()
           + "\n      z informacja: " + e.getMessage());
      }
    }
  }

  static int wykonaj(int i) throws Exception
  { if (i == 1) 
    { int x = 0;
      return x / x;                       // dzielenie przez 0
    }
    if (i == 2) 
    { String s = null;
      return s.length();                  // długość pustej zmiennej
    }
    if (i ==3 )
    { int t[] = new int[10];
      return t[10];                       // modyfikator poza tablicą
    }
    return 0;
  }
}

W tak napisanym programie każde wystąpienie błędu w metodzie wykonaj spowoduje przejście do bloku catch w metodzie main. Komunikaty wyświetlone przez nasz program powinny wyglądać tak:

Hierarchia wyjątków

Musisz pamiętać, że wyjątki w Javie stanowią pewną hierarchię: od najbardziej ogólnych do bardzo szczegółowych. W sytuacji, gdy wyjatek wystapi wykonywany jest tylko jeden blok catch: pierwszy, którego typ parametru jest zgodny z typem wyjątku. Dlatego też należy pamiętać, aby szczegółowe bloki catch umieszczać na początku, a bardziej ogólne - dalej.

Najbardziej typowe błędy kolejności bloków obsługi wyjątków sygnalizowane są przez kompilator jako błędy. Gdybyśmy w jednym z poprzednich programów umieścili bloki obsługi wyjątków w takiej kolejności:

      try
      { ..... } 

      catch (Exception e)
      { ..... }

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

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

      catch (ArrayIndexOutOfBoundsException e) 
      { ..... }
}

to praktycznie zawsze wykonywany byłby tylko pierwszy blok catch, gdyż klasa Exception jest najogólniejszą klasą wyjątków. Jednak kompilator nie dopuści do tego, sygnalizując błędy:

exception java.lang.ArithmeticException has already been caught
exception java.lang.NullPointerException has already been caught
exception java.lang.ArrayIndexOutOfBoundsException has already been caught
« wstecz   dalej »