« poprzedni punkt  następny punkt »

4. Wyjątki

Jak wiemy, programowanie w Javie sprowadza się tak naprawdę do tworzenia obiektów i wywoływania na ich rzecz metod (albo też - gdy nie mamy obiektów - wywoływania metod statycznych). W trakcie wykonania metody może powstać jakiś błąd.
W wielu innych językach (np. w C czy C++) błędy powstają w trakcie wykonywania funkcji.
Informacja o tym, że wystąpił błąd winna być dostępna dla innych (korzystających z danej funkcji czy metody) części programu i służyć powinna do odpowiedniego reagowania na powstały błąd np. poinformowania użytkownika o błędzie lub próby jego poprawienia (poprzez zmianę jakichś wartości) bądź też do przerwania programu.
Sposoby w jaki błędy są sygnalizowane innym częściom programu, a także  decyzje i działania, które programista zapisuje w programie jako reakcje na powstałe błędy nazywają się obsługą błędów.

Tradycyjna obsługa błędów (a raczej ich sygnalizowanie)  polegało na:

  • zwracaniu przez funkcję wartości true | false | int, świadczących o tym, czy wykonanie zakończyło się sukcesem, czy też powstał błąd (i jaki ew. jest jego kod),
  • ustawianiu przez funkcję, w trakcie wykonania której pojawił się błąd, flag błędów, dostępnych z innych funkcji (w Javie flagi takie moglibyśmy definiować jako pola klasy).

Problemy, które występują przy takim podejściu:

  • trzeba pamiętać, żeby sprawdzić, czy wystąpił błąd (np. pamiętać o sprawdzeniu wyniku metody),
  • nie ma standardowych, zunifikowanych środków sygnalizowania błędów i różni programiści robią to w różny sposób,
  • można pominąć sprawdzanie czy wystąpił błąd.

W Javie (poszerzającej doświadczenia języka C++) zaproponowany nowy sposób obsługi
błędów - za pomocą obsługi wyjątków.

Wyjątek - to sygnał o błędzie w trakcie wykonania programu

Wyjątek powstaje na skutek jakiegoś nieoczekiwanego błędu.
Wyjątek jest zgłaszany (lub mówiąc inaczej - sygnalizowany).
Wyjątek jest (może lub musi być) obsługiwany.

Prosty schemat obsługi wyjątków

    try {
        // ... w bloku try ujmujemy instrukcje, które mogą spowodować wyjątek
    } catch(TypWyjątku exc)  {
        // ... w klauzuli catch umieszczamy obsługę wyjątku
    }
     
Gdy w wyniku wykonania instrukcji w bloku try powstanie wyjątek typu TypWyjątku  to sterowanie zostanie przekazane do kodu umieszczonego w w/w klauzuli catch

Przykłady.

a) Brak jawnej obsługi wyjątku - powstały błąd (wyjątek) powoduje zakończenie programu, a JVM wypisuje komunikat o jego przyczynie.

public class NoCatch {

  public static void main(String[] args) {
    int a = 1, b = 0, c = 0;
    c = a/b;
    System.out.println(c);
  }

}

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at NoCatch.main(NoCatch.java:6)

b) Zabezpieczamy się przed możliwymi skutkami całkowitoliczbowego dzielenia przez zero, obsługując wyjątek ArithmeticException

public class Catch1 {

  public static void main(String[] args) {
    int a = 1, b = 0, c = 0;
    String wynik;
    try {
      c = a/b;
      wynik = "" + c;
    } catch (ArithmeticException exc) {
        wynik = "***";
    }
    System.out.println(wynik);
  }

}

W tym przypadku, wykonanie instrukcji c = a/b; spowoduje powstanie wyjątku (dzielenie przez zero), a ponieważ instrukcja ta znajduje się w bloku try, do którego "podczepiona" jest klauzula catch z odpowiednim typem wyjątku, to sterowanie zostanie przekazane do kodu w catch, zmienna wynik uzyska wartość "***", i wynik ten zostanie wyprowadzony na konsolę. Gdyby zmienna b nie miała wartości zero, wyjątek by nie powstał, kod w klauzuli catch nie został by wykonany i na konsolę wyprowadzony by został wynik dzielenia a/b.

Mechanizm obsługi wyjątków może być wykorzystywany w bardzo różny i elastyczny sposób.
Typowym przykładem jest weryfikacja wprowadzanych przez użytkownika danych.
Wielokrotnie w dotąd omawianych przykładowych programach żądaliśmy od użytkownika wprowadzania liczb całkowitych, a następnie za pomocą metody parseInt przeksztalcaliśmy ich znakową reprezentację na binarną. Jak wiemy, jeśli przy tym wprowadzony napis nie reprezentuje liczby całkowitej, to powstaje wyjątek NumberFormatException. Powinniśmy go zawsze obsługiwać.
Możemy więc teraz  zmodyfikować np. program wykonywania operacji arytmetycznych na liczbach całkowitych:

import java.util.*;
import javax.swing.*;

public class Oper {

  public static void main(String[] args) {

    String normalQuest = "Liczba1 op Liczba2",
           errorQuest = "Wadliwe dane. Jeszcze raz.\n" + normalQuest,
           quest = normalQuest;

    String expr;
    int num1 = 0, num2 = 0, res = 0;

    while ((expr = JOptionPane.showInputDialog(quest)) != null) {

      StringTokenizer st = new StringTokenizer(expr);

      if (st.countTokens() != 3) {
          quest = errorQuest;
          continue;
      }

      String snum1 = st.nextToken(),
             sop  = st.nextToken(),
             snum2 = st.nextToken();

      try {
        num1 = Integer.parseInt(snum1);
        num2 = Integer.parseInt(snum2);
      } catch (NumberFormatException exc) {
          quest = errorQuest;
          continue;
      }

      char op = sop.charAt(0);

      switch (op) {
        case '+' : res = num1 + num2; break;
        case '-' : res = num1 - num2; break;
        case '*' : res = num1 * num2; break;
        case '/' : res = num1 / num2; break;
        default: {
          quest = errorQuest;
          continue;
        }
      }
      JOptionPane.showMessageDialog(null, "Wynik = " + res);
      quest = normalQuest;
    }
    System.exit(0);

  }

}

A cóż to jest NumberFormatException albo ArithmeticExcception? I dlaczego w klauzuli catch używamy takich nazw z dodatkiem czegoś, co wygląda jak zmienna np. catch (NumberFormatException exc) ...

Otóż wyjątki są obiektami klas wyjątków.

r

             (Żródło: Peter Haggar, Java Exception Handling, IBM 1999)

Zatem nazwy NumberFormatException, ArithmeticException itd. sa nazwami klas, a zmienna exc we wczesniejszych przykładach jest faktycznie zmienną - zawiera referencję do obiektu odpowiedniej klasy wyjątku.

Wobec tej zmiennej możemy np. użyć metody toString() uzyskując jako wynik jej zastosowania opis wyjątku, taki jaki daje JVM, gdy wyjątek jest nieobsługiwany.

Nie zawsze jednak możemy uniknąć obsługi wyjątku.

WYJĄTKI KONTROLOWANE I NIEKONTROLOWANE

  • Są dwa rodzaje wyjątków: kontrolowane i niekontrolowane
  • Wyjątki pochodne od klas RuntimeException i Error są niekontrolowane:
    • oznaczają one błędy fazy wykonania (mniej poważne i poważne),
    • mogą wystąpić w dowolnym miejscu kodu.
  • Pozostałe wyjątki są kontrolowane, co oznacza, że:
    • metody zgłaszające te wyjątki wymieniają je jawnie w swojej deklaracji w klauzuli throws,
    • metody te mogą zgłaszać tylko wymienione w klauzuli throws  wyjątki lub wyjątki ich podklas,
    • odwołania do tych metod wymagają jawnej obsługi ew. zgłaszanych wyjątków:
      • poprzez konstrukcje try - catch,
      • poprzez wymienienie wyjątku w klauzuli throws naszej metody (tej która odwołuje się do metody, która może zgłosić wyjątek) i "przesunąć" obsługę wyjątku do miesca wywołania naszej metody.

Wiele razy natkniemy się na sytuację, w której musimy obslugiwać wyjątki, które mogą powstać przy wywołaniau jakichś metod ze standardowych klas Javy. Jeśli tego nie zrobimy, kompilator wykaże błąd w programie. Sytuacja taka dotyczy, na przykład, metod ze standardowego pakietu  java.io, zawierającego klasy do operowania na strumieniach danych (m.in. plikach).

Przykład (jeśli okaże się niezrozumiały, proszę wrócić do niego po lekturze następnego punktu - o plikach) :

 String inFname = ...;  // nazwa pliku wejściowego
 String outFname = ... ; // nazwa pliku wyjściowego
 FileInputStream in;        // pik wejściowy
 FileOutputStream out;     // plik wyjściowy
 try {
   in  = new FileInputStream(inFname);
   out = new FileOutputStream(outFname);
   int c = 0;
   while ((c = in.read()) != -1) out.write(c);   // czytanie in bajt po bajcie
                                                                // zapis kolejnych bajtów do out

 } catch(FileNotFoundException exc) {  // obsługa błędu:  nieznany plik
     System.out.println("Plik " + inFname + " nie istnieje.");
     System.exit(1);
 }
 } catch(IOException exc) {   // obsługa błędu:  inny błąd wejścia-wyjścia
     System.out.println(exc.toString()); //
     System.exit(1);
 }  

Gdybyśmy napisali metodę kopiującą strumienie i nie obsługiwali w niej wyjątków wejścia-wyjścia - to musielibyśmy zaznaczyć, że przy wywołanie takiej metody moga powstać wyjątki klasy IOException:

public static void copyStream(InputStream in, OutputStream out)
              throws IOException {

  int c = 0;
  while ((c = in.read()) != -1) out.write(c);
}

a obsługa wyjątku IOException, który może powstać przy wywołaniu read() musiałaby być prowadzona w miejscu wywołania metody copyStream(...):

try {
   .....
   copyStream(in, out);
} catch(IOException exc) { ...  } 


Warto zwrócić w tym momencie uwagę na to, że w poprzednim przykładzie pojawiło się kilka klauzuli catch odpowiadających jednemu blokowi try.
 

SEKWENCJA DZIAŁANIA try-catch

  • Wykonywane są kolejne instrukcje bloku try.
  • Jeśli w którejś instrukcji wystąpi błąd (na skutek czego powstanie wyjątek), wykonanie bloku try jest przerywane w miejscu wystąpienia błędu.
  • Sterowanie przekazywane jest do pierwszej w kolejności klauzuli catch, w której podana w nawiasach okrągłych po słowie catch klasa wyjątku pasuje do typu powstałego wyjątku:
    • Stąd ważny wniosek: najpierw podawać BARDZIEJ SZCZEGÓŁOWE TYPY WYJĄTKÓW np. najpierw FileNotFoundException, a później IOException, bo klasa FileNotFoundException jest pochodna od IOException
  • Inne klauzule catch nie są wykonywane.
  • Obsługująca wyjątek klauzula catch może zrobić wiele rzeczy: m.in. zmienić sekwencję sterowania (np. poprzez return lub zgłoszenie nowego wyjątku za pomocą instrukcji throw). Jeśli nie zmienia sekwencji sterowania to wykonanie programu jest kontynuowane od następnej instrukcji po bloku try.


KLAUZULA FINALLY

Klauzula finally służy do wykonania kodu niezależnie od tego czy wystąpił wyjątek czy nie.

boolean metoda(...) {
try {
       // instrukcje, które mogą spowodować wyjątek
}
catch(Exception e) {  return false; }
finally {
       // uporządkowanie, np. zamknięcie pliku
       }
return true;
}

Jeśli powstał wyjątek - wykonywana jest klauzula catch.
Mimo, iż zmienia ona sekwencję sterowania (zwraca false na znak, iż nastąpiło niepowodzenie), sterowanie przekazywane jest do klauzuli finally. I dopiero potem zwracany jest wynik - false.
Jeśli nie było wyjątku, po zakończeniu instrukcji w bloku try sterowanie od razu wchodzi do klauzuli finally, a po jej zakończeniu zwracany jest wynik true (wykonywana jest ostatnia instrukcja metody).
 

WŁASNE WYJĄTKI

Wyjątki są obiektami klas pochodnych od Throwable.
Żeby stworzyć własny wyjątek należy zdefiniować odpowiednią klasę.
Zgodnie z konwencją dziedziczymy podklasę Throwable - klasę Exception.

class NaszWyj extends Exception {
...
}

Zwykle w naszej klasie wystarczy umieścić dwa konstruktory: bezparametrowy oraz z jednym argumentem typu String (komunikat o przyczynie powstania wyjątku). W konstruktorach tych nalezy wywołać konstruktor nadklasy (za pomocą odwołania super(...), w drugim przypadku z argumentem String).

Użycie wyjątku:

  • jakaś nasza metoda ma sygnalizować wyjątek NaszWyj -- musi podać w deklaracji, że może to zrobić:
    • void naszaMetoda() throws NaszWyj
  • nasza metoda sprawdza warunki powstania błędu
  • jeśli jest błąd - tworzy wyjątek (new NaszWyj(...)) i sygnalizuje go za pomocą instrukcji throw :
    • throw new NaszWyj(ew_param_konstruktora_z_info_o_błędzie)

Poniższy przykład ilustruje wyżej powiedziane.
W klasie ZipAsk zdefiniowano metodę wprowadzania kodu pocztowego (getZip). W metodzie tej sprawdzana jest poprawność struktury wprowadzonego kodu (nn-nnn, gdzie n - cyfry). Jeżeli kod nie jest poprawny, zgłaszany jest wyjątek własnej klasy NotValidZipException. Metoda main klasy ZipAskTest służy do przetestowania działania: obsługujemy w niej wyjątek NotValidZipException, zmuszając użytkownika programu do wprowadzenia trzech poprawnych kodów (lub ew. rezygnacji z działania poprzez wybór Cancel w dialogu).

 import javax.swing.*;

class NotValidZipException extends Exception {   // Klasa wyjątku

    NotValidZipException() {
      super();
    }

    NotValidZipException(String s)  {
      super(s+ "\nPoprawny kod ma postać: nn-nnn");
    }
}


public class ZipAsk {

 public ZipAsk() { }

 public String getZip() throws NotValidZipException {

    final int N = 6,        // długość kodu
              P = 2;        // pozycja na której występuje kreska

    String zip = JOptionPane.showInputDialog("Podaj kod pocztowy:");
    if (zip == null) return zip;

    boolean valid = true;   // czy kod poprawny?

    char[] c = zip.toCharArray(); // tablica znaków w podanym kodzie

    // jeżeli struktura wadliwa: nie ta długość, brak kreski
    if (c.length != N || c[P] != '-') valid = false;

    // czy w kodzie występują tylko cyfry?
    for (int i = 0; i<N && valid; i++) {
         if (i==P) continue;
         if (!Character.isDigit(c[i])) valid = false;
         }
    // w tej chwili wiemy już, czy kod jest poprawny
    // jeśli nie:
    // - tworzymy i zgłaszamy wyjątek
    if (!valid) throw new NotValidZipException("Wadliwy kod: " + zip);

    // w przeciwnym razie zwracamy kod
    return zip;
    }
}

class ZipAskTest {

  public static void main(String[] args) {

    JOptionPane.showMessageDialog(null, "Podaj trzy prawidłowe kody pocztowe");

    ZipAsk zask = new ZipAsk();
    String zip = null;
    int n = 3;

    while (n > 0) {
      try {
        zip = zask.getZip();
        if (zip == null) break;
        n--;
      } catch (NotValidZipException exc) {
          JOptionPane.showMessageDialog(null, exc.getMessage());
          continue;
      }
      System.out.println("Kod " + (3-n) + " : " + zip);
    }
    System.exit(0);

  }

} 


« poprzedni punkt  następny punkt »