[WikiDyd] [TitleIndex] [WordIndex

Obsługa błędów w Javie - wyjątki

Mówiąc o obsłudze błędów mamy tu na myśli sytuacje wyjątkowe powstałe w czasie działania programu: napotkanie błędnych danych (brak/uszkodzony plik, niewłaściwe wartości argumentów, problemy z komunikacją z urządzeniami zewnętrznymi, itp).

Dla przykładu wyobraźmy sobie program w C, który kopiuje pliki

   1 #include <stdio.h>
   2 
   3 main ( int argc, char ** argv ) {
   4    int c;
   5    FILE *in= argc > 1 ? fopen( argv[1], "r" ) : stdin;
   6    FILE *ou= argc > 2 ? fopen( argv[2], "w" ) : stdout;
   7 
   8    while( (c= fgetc( in )) != EOF ) 
   9       fputc( c, out );
  10    fclose( in );
  11    fclose( ou );
  12 
  13    return 0;
  14 }

A teraz dodajmy do tego kodu obsługę błędów:

   1 #include <stdio.h>
   2 
   3 main ( int argc, char ** argv ) {
   4    int c;
   5    FILE *in= argc > 1 ? fopen( argv[1], "r" ) : stdin;
   6    FILE *ou= argc > 2 ? fopen( argv[2], "w" ) : stdout;
   7 
   8    if( in == NULL ) {
   9      /* kod obslugi */
  10    }
  11    if( ou == NULL ) {
  12      /* kod obslugi */
  13    }
  14 
  15    while( (c= fgetc( in )) != EOF ) 
  16       if( fputc( c, out ) == EOF ) {
  17         /* kod obslugi */
  18       }
  19    if( ferror( in ) ) {
  20      /* kod obslugi */
  21    }
  22    if( fclose( in ) == EOF ) {
  23      /* kod obslugi */
  24    }
  25    if( fclose( ou ) == EOF ) {
  26      /* kod obslugi */
  27    }
  28 
  29    return 0;
  30 }

i mamy program, w którym (zwłaszcza po dodaniu kodu OB) trudno zauważyć, co właściwie jest robione.

Mechanizm wyjątków w Javie pozwala:

1. zachować możliwość reagowania na błędy przy niezmienionej czytelności kodu

2. umożliwić "eleganckie" wyjście z sytuacji awaryjnej, która wystąpiła w mocno zagnieżdżonym kodzie

3. "wymusić" na programiście obsługę określonych sytuacji wyjątkowych

4. tworzyć kod, który precyzyjnie diagnozuje błędy.

Reagowanie na błędy i czytelność kodu

Rozpatrzmy następujący fragment programu:

   1    try {
   2       f= new FileInputStream( args[0] );
   3       while( ! f.eof() ) {
   4          we= f.read();
   5          // kod, kod, kod - wiele linii
   6       }
   7    } catch ( FileNotFoundException e ) {
   8       System.err.printl( "MyClass: input problem: " + e );
   9    } catch ( IOException e ) {
  10       System.err.printl( "MyClass: input problem: " + e );
  11    } finally {
  12       if( f != null )
  13          f.close();
  14    }

Java obsługuje sytuacje potencjalnie niebezpieczne według schematu

PRÓBUJ -> WYCHWYĆ -> OBSŁUŻ

Kod, w którym mogą wystąpić sytuacje wyjątkowe umieszczamy w bloku po słowie kluczowym "try". Jeżeli wewnątrz takiego kodu (tzn. przy wykonywaniu którejkolwiek z instrukcji z tego bloku) zostanie "zgłoszony" wyjątek, to JVM przerywa wykonywanie instrukcji z bloku i przechodzi do sekwencji "catch", szukając takiej, której wyjątek pasuje do zgłoszonego.

Co to znaczy "pasuje" ? - Wyjątki też są obiektami pochodzącymi od klasy Exception, która pochodzi od klasy Throwable.

Znaczy to, że wyjątki tworzą hierarchię klas.

Wyjątek "pasuje" do zgłoszonego, jeśli jego klasa jest taka sama, lub jest nadklasą wyjątku zgłaszanego.

W powyższym przykładzie FileNotFoundException jest klasą pochodną od IOException i dlatego musi wystąpić jako pierwszy w sekwencji catch -- w przeciwnym wypadku taki wyjątek byłby wyłapany przez klauzulę catch( IOException ...

Klauzula finally jest opcjonalna, ale jeżeli występuje, to jej blok jest wykonywany ZAWSZE, niezależnie od tego, czy wyjątek (jakikolwiek) wystąpił, czy nie.

Eleganckie wyjście z sytucji po błędzie

Załóżmy, że piszemy analizator składni wyrażeń w notacji wroskowej. Taki analizator ma zwykle wiele zagłębionych pętli, rekurencyjnych wywołań funkcji, itp. itp. Jeżeli w trakcie zagłębiania się w "składniki w czynnikach w nawiasach, w czynnikach, w składnikach..." napotkamy nagle koniec napisu, który analizujemy (bo ktoś nie dopisał go do końca), to powrót z takiego zagłębionego wywołania wymaga pisania pracochłonnego kodu, który sprawdza wyniki każdej wywoływanej funkcji. Przekazanie w górę informacji o znalezionym błędzie, to osobny bolesny problem.

Wykorzystanie w takiej sytuacji wyjątku pozwala "przeskoczyć" przez wiele pośrednich etapów (wyrzucić ze stosu wiele rekordów aktywacji - na wykładzie mówię tutaj, co to jest rekord aktywacji :-) do miejsca, gdzie ta sytuacja ma być obsłużona. Mechanizm zgłoszenia wyjątku wygląda mniej-więcej tak:

   1   public ParseTree ExprParser( String expr ) 
   2      throws ParseException
   3   {
   4      // petle, petle, petle
   5        if( cosJestZle ) {
   6           throw new ParseException();
   7        }
   8      // kod, kod, kod
   9   }

Instrukcja throw zgłasza wyjątek i natychmiast opuszcza metodę ExprParser. Jest to podobne do instrukcji return ale:

Wymuszenie reakcji na ewentualne błędy wykonania

Pisząc metodę ExprParser musimy zadeklarować wszystkie wyjątki, które ta metoda zgłasza. Jeżeli coś wywołuje metodę zgłaszającą wyjątki, to musi te wyjątki obsłużyć, lub zadeklarować je jako zgłaszane. Takie podejście umożliwia orientację we wszystkich sytuacjach potencjalnie niebezpiecznych i wymusza obsługę takich sytuacji.

Kompilator nie skompiluje kodu, który ignoruje wyjątki zgłaszane w wywoływanych przez ten kod metodach. Przykład:

   1 import java.io.*;
   2 
   3 class Test1 {
   4    public static void main( String [] args ) {
   5       FileInputStream f= null;
   6       int we;
   7       try {
   8          f= new FileInputStream( args[0] );
   9          we= f.read();
  10          System.out.println( we );
  11       } catch ( FileNotFoundException e ) {
  12          System.err.println( "MyClass: input problem: " + e );
  13       } catch ( IOException e ) {
  14          System.err.println( "MyClass: input problem: " + e );
  15       } finally {
  16          f.close();
  17       }
  18    }
  19 }

I wynik próby kompilacji powyższego pliku

c:\home\jstar\java\moje\w4>javac Test1.java
Test1.java:16: Exception java.io.IOException must be caught, or it must be
declared in the throws clause of this method.
         f.close();
                ^
1 error

Precyzyjna diagnostyka błędów

Możliwość deklarowania własnych wyjątków umożliwia przekazanie dokładnej informacji o błędach. Często wystarczy nazwa wyjątku i napis (dziedziczony od Exception), ale czasem dobrze jest też podać inne dane.

   1 class DictionaryException extends Exception {
   2    Dictionary d;
   3 
   4    DictionaryException( String whatsWrong, Dictionary where ) {
   5       super( whatsWrong );
   6       d= where;
   7    }
   8 }
   9 
  10 // .....
  11 
  12 public processData( /* argumenty */ ) throws DictionaryException {
  13    Distionary tmp= new Dictionary( 500 );
  14 
  15    // tu uzywamy tmp roboczo
  16 
  17    if( tmp.jakisBlad() ) {
  18       throw DictionaryException( "tu opis bledu", tmp );
  19    }
  20 
  21    // cos tam innego
  22 
  23    return;  // tu referencja do tmp przestaje istniec
  24 }

W takiej konstrukcji, w razie wystąpienia błędu tmp jest zachowywany i kod, który błąd obsługuje może np. przeegzaminować jego zawartość.

Definiowanie wyjątku

Możemy zdefiniować swój własny wyjątek. Na przykład zdefiniujmy wyjątek, który zgłosimy w sytuacji, gdy chcemy zrobić coś brzydkiego z listą liniową. Taki wyjątek będzie na przykład zgłaszany, gdy będziemy próbowali wyjąć element z pustej listy, lub przejść w itercji poza jej koniec. Nasz wyjątek zawiera jedynie napis, który opisuje błąd.

   1 /* Lista liniowa jednokierunkowa, wersja poprawiona,
   2    wykorzystująca wyjątki do zgłaszenia błędów
   3 */
   4 
   5 class EmptyListException extends Exception {
   6    EmptyListException( String s ) {
   7      super( s );   // widać,
   8                    // że właściwie potrzebujemy tylko nazwy wyjątku
   9    }
  10 }
  11 
  12 class List3 {
  13 
  14    private class LNode {
  15       Object item;
  16       LNode next;
  17 
  18       LNode( Object item ) {
  19          this.item= item;
  20          this.next= null;
  21       }
  22 
  23       LNode( Object item, LNode next ) {
  24          this.item= item;
  25          this.next= next;
  26       }
  27 
  28       void insertAfter( Object item ) {
  29          next= new LNode( item, next );
  30       }
  31    }
  32 
  33    private LNode head;
  34    private int size;
  35    private LNode iterator;
  36     
  37    List3() {
  38       head= null;
  39       iterator= null;
  40       size= 0;
  41    }
  42 
  43    public void insertAtFront( Object newItem ) {
  44       head= new LNode( newItem, head );
  45       size++;
  46    }
  47 
  48    public void insertAtEnd( Object newItem ) {
  49       if( head == null ) {
  50          head= new LNode( newItem, head );
  51          size= 1;
  52       } else {
  53          LNode p= head;
  54          while( p.next != null )
  55             p= p.next;
  56          p.next= new LNode( newItem, p.next );
  57          size++;
  58       }
  59    }
  60 
  61    public Object removeFirst( ) 
  62       throws EmptyListException
  63    { // zwraca usuniety element
  64       if( head != null ) {
  65          Object tmp= head.item;
  66          head= head.next;
  67          size--;
  68          return tmp;
  69       } else {
  70          throw new EmptyListException( "Trying to remove first element from an empty list" );
  71       }
  72    }
  73 
  74    public Object removeLast( )
  75       throws EmptyListException
  76    { // zwraca usuniety element
  77       if( head != null ) {
  78          if( head.next == null )
  79             return this.removeFirst();
  80          else {
  81             LNode p= head;
  82             while( p.next.next != null )
  83                p= p.next;
  84             Object tmp= p.next.item;
  85             p.next= null;
  86             return tmp;
  87          }
  88       } else {
  89          throw new EmptyListException( "Trying to remove last element from an empty list" );
  90       }
  91    }
  92 
  93    public void resetIterator() {      
  94       iterator= head;
  95    }
  96 
  97    public boolean isNext() {
  98       return iterator != null;
  99    }
 100 
 101    public Object getNext()
 102       throws EmptyListException
 103    {
 104       if( isNext() ) {
 105          Object tmp= iterator.item;
 106          iterator= iterator.next;
 107          return tmp;
 108       } else
 109          throw new EmptyListException( "Trying to iterate past the end of list" );
 110    }
 111  
 112    public String toString( ) {
 113       StringBuffer b= new StringBuffer( "( " );
 114 
 115       for( LNode p=head; p != null; p= p.next ) 
 116          b.append( p.item + " " );
 117       b.append( " )" );
 118       return b.toString();
 119    }
 120 
 121    public static void main( String [] args ) {
 122       try {
 123          List3 l= new List3();
 124 
 125          for( int i=1; i <= 4; i++ )
 126             l.insertAtEnd( new Integer( i ) );
 127 
 128          System.out.println( l );
 129 
 130          for( double d=1; d <= 6; d++ )
 131             l.insertAtFront( new Double( d ) );
 132 
 133          System.out.println( l );
 134 
 135          l.resetIterator();
 136          while( l.isNext() ) {
 137             Object o= l.getNext(); 
 138             if( o instanceof Double )
 139                System.out.println( "Double: " + o );
 140             else if( o instanceof Integer )
 141                System.out.println( "Integer: " + o );
 142             else
 143                System.out.println( "other: " + o );
 144          }
 145 
 146 
 147          for( int i=1; i<=3; i++ ) {
 148             l.removeFirst();
 149             l.removeLast();
 150          }
 151          System.out.println( l );
 152          List3 ll= new List3();
 153          for( int i=10; i < 40; i+= 10 )
 154             ll.insertAtEnd( new Integer( i ) );
 155          l.insertAtFront( ll );
 156          System.out.println( l );
 157          l.resetIterator();
 158          while( l.isNext() ) {
 159             Object o= l.getNext(); 
 160             if( o instanceof Double )
 161                System.out.println( "Double: " + o );
 162             else if( o instanceof Integer )
 163                System.out.println( "Integer: " + o );
 164             else
 165                System.out.println( "other: " + o );
 166          }
 167          // Ten kod testuje obsluge wyjatkow:
 168          List3 pusta= new List3();
 169          // Trzeba wybrac to, co chcemy testowac
 170          // 1
 171          // pusta.removeFirst();
 172          // 2
 173          // pusta.removeLast();
 174          // 3
 175          pusta.insertAtEnd( new Integer( 10 ) );
 176          pusta.insertAtEnd( new Float( 10.0 ) );
 177          pusta.insertAtEnd( new Double( 10.0 ) );
 178          pusta.resetIterator();
 179          for( int i= 0; i <=3; i++ )
 180             System.out.println( pusta.getNext() );
 181          //koniec wyborow
 182       } catch ( EmptyListException e ) {
 183          System.err.println( "Error while testing List3: " + e.getMessage() );
 184       }
 185    }
 186 }

Jako ćwiczenie proszę zmienić definicje wyjatku tak, aby wyjatek zawierał też listę, w której wystapił błąd.

Hierarchia wyjątków

                      Object
                         |
                      Throwable
                     /         \
                   /           Exception
                 /            /         \
            Error     RuntimeException   ...
           / ..  \   / ...    \

Wyjątki klasy Error sygnalizują poważne błędy, które (raczej) są nienaprawialne (ClassFormatError, IllegalAccessError, NoSuchMEthodError, StackOverflowError, ... zob. Dodatek B w [Java]).

Wyjątki klasy RuntimeException są błędami czasu wykonania, zgłaszanymi przez JVM (ArithmeticException, IndexOutOfBoundsException, SecurityException, NumberFormatException, ClassCastException,... zob. j.w.)

Wyjątki klas Errror i RuntimeException nie są kontrolowane przez kompilator. Zakłada się, że mogą być zgłoszone wszędzie i deklarowanie ich obsługi byłoby niewygodne. Nie znaczy to jednak, ze nie możemy ich wyłapywać i obsługiwać. Po prostu nie musimy ich zgłaszać w "throws"/"catch".

Teoretycznie można takie klasy rozszerzać, ale nie należy tego robić, bo lepiej jest, gdy pełny opis zachowania metody można wyczytać z jej deklaracji. Te wyjątki zastrzeżone są zwyczajowo dla JVM.


2015-09-23 06:44