Behandlung von Ausnahmen (Abfangen)

<-- zurck zur Startseite | <- eine Seite vor | eine Seite weiter ->

  1. Ausnahmen abfangen
  2. mehrere catch-Blöcke
  3. der finally-Block
  4. Übung
  5. Lösung

1. Ausnahmen abfangen

Die zweite Möglichkeit, Ausnahmen zu behandeln, ist - laut der catch-or-throw-Regel - das "Abfangen" der Ausnahme. Hierzu dienen die sog. try-catch-Blöcke. Syntax:

try {
  //diverse Anweisungen, die Ausnahmen erzeugen k&#246;nnen
} catch (Ausnahmetyp objektname) {
  //Anweisungen zur Behandlung der Ausnahme
}

Der try-Block enthält eine bzw. mehrere Anweisungen, von denen mindestens eine eine Ausnahme auslösen kann. Ist dies der Fall, so wird in den catch-Block gesprungen, wo der Fehler entsprechend unseren Vorgaben weiterverarbeitet werden kann. In Klammern hinter dem Schlüsselwort catch wird der entsprechende Ausnahmetyp deklariert, also den Namen der Exception-Klasse, und dem Ausnahme-Objekt ein beliebiger Name zugeteilt, über den wir auf die Ausnahme zugreifen können.
Jedes Ausnahme-Objekt wurde auf der Klasse Throwable oder einer Unterklasse davon erzeugt und hat damit in jedem Fall folgende Methoden zur Verfügung:

public String getMessage()
public void printStackTrace()
public String toString()

Während die Methode printStackTrace den Fehler im Laufzeit-Stack zurückverfolgt, liefert die Methode getMessage die Fehlerursache zurück.Wenn der Programmierer selbst eine Ausnahme mit throw (siehe vorheriges Kapitel) ausgelöst hat, liefert sie exakt den gesetzten Fehlertext zurück!
Mit der Methode toString kann ebenfalls ein einfacher Fehlertext abgeholt werden: er besteht aus dem Namen der zugehörigen Fehlerklasse und dem Verursacher des Fehlers.
Von der Theorie zur Praxis:

public class FehlerTest5 {
   public static void main(String[] args) {
     try {
       int zahl = Integer.parseInt("s");
       System.out.println("Quadrat unserer Zahl: " + (zahl*zahl));
     } catch (NumberFormatException nfe) {
       System.out.println("Art des Fehlers: " + nfe.toString());
       System.out.println("Beschreibung: " + nfe.getMessage());
       System.out.println("Stacktrace: ");
       nfe.printStackTrace();
     }
   }
}

Die Methode parseInt löst hier eine NumberFormatException, die mit dem catch-Block abgefangen wird. Das Ausnahme-Objekt habe ich "nfe" genannt (aus den Anfangsbuchstaben der Ausnahmeklasse). Im catch-Block lassen wir uns dann detailliert die Art des Fehlers anzeigen (wobei man sich in der Praxis meist mit einer der Methoden zur Fehlertext-Ausgabe begnügt). Auffallend ist dabei die Ausgabe des Stacktrace (hier unter Linux):

screenshot

Wer aufmerksam die Quelltexte aus dem letzten Kapitel ausprobiert hat, dürfte diese Ausgabe bereits kennen. Begnügt man sich damit, den Fehler im gesamten Programm nur mit "throws" weiterzureichen oder gar nicht zu deklarieren, wird ebenfalls der "Stacktrace" ausgegeben. Die Methode "printStackTrace" wird dann vor dem Beenden des Programms von der Java-Laufzeitumgebung (JVM) aufgerufen. Aus dem Stacktrace kann man so den genauen Verursacher feststellen. Dieser steht in der letzten Zeile und ist so zu lesen: in der Klasse "FehlerTest4", Methode "main" (im Quelltext "FehlerTest4.java": Zeile 4. Der Stacktrace erleichtert so die Fehlersuche.
(NB: Den Stack kann man sich wie einen Stapel Spielkarten vorstellen. Man kann nur oben drauflegen und auch von oben wieder wegnehmen. Wenn der Rechner beim Ausführen eines Programms auf eine Methode bzw. Funktion trifft, so muss er nachher wieder wissen, wohin er zurückkehren soll. Deswegen wird die Adresse der aufrufenden Stelle auf den Stack im Rechner gelegt, so dass bei Beendigung der Funktion das Programm wieder an die richtige Stelle zurückfindet. Dann wird die Adresse wieder vom Stack genommen. Auf diese Weise ist es sehr einfach, einen Fehler durch die aufrufenden Stellen zurückzuverfolgen: man braucht sich nur den Stack ausschnittsweise ansehen.)

Werfen wir einen erneuten Blick auf obigen Quelltext: der try-Block umfasst hier nicht nur den Verursacher der Ausnahme selbst, sondern auch eine weitere Anweisung, die mit dem Zahlenwert weiterarbeiten sollte. Dass diese ebenfalls im try-Block stehen muss, ergibt sich daraus, dass es keinen Sinn macht, das Quadrat zu berechnen und die Ausgabe zu starten, wenn wir überhaupt keine Zahl ermitteln können. Die Ausgabe erfolgt dann nur, wenn wirklich eine Integer-Zahl ermittelt wurde, andernfalls wird nur der Fehler ausgedruckt.
Ich hatte ja schon einmal erwähnt, dass der Sinn der Fehlerbehandlung auch darin liegt, das Programm trotz eines Fehlers weiterlaufen zu lassen. Bis jetzt haben wir uns nur den Fehler ausdrucken lassen, aber dazu reicht ja im Grunde auch das Weiterreichen der Ausnahme. Im catch-Block kann jedoch auch Programmcode ausgeführt werden, der es dem Programm ermöglicht, seine Arbeit fortzusetzen. Für unser Beispiel könnten wir, wenn die Umwandlung in ein Integer fehl schlägt (z.B. aufgrund einer fehlerhaften Benutzereingabe), Standardwerte zuweisen. Somit kann das Programm in jedem Fall weiter ausgeführt werden:

public class FehlerTest6 {
   public static void main(String[] args) {
     int zahl;
     try {
       zahl = Integer.parseInt("s");
     } catch (NumberFormatException nfe) {
       System.out.println("Art des Fehlers: " + nfe.toString());
       System.out.println("\nSetze Wert auf 4!");
       zahl = 4;
     }
     System.out.println("Quadrat unserer Zahl: " + (zahl*zahl));
   }
}

Kennt man den exakten Namen der Fehlerklasse mal nicht, so kann man sich wie beim Weiterreichen von Fehlern damit behelfen, dass man eine Ausnahme vom Typ Exception oder Throwable abfängt. Allerdings sollte man dies nicht aus Schreibfaulheit tun, sondern bei Kenntnis des genauen Ausnahmetyps diesen auch verwenden.

public class FehlerTest7 {
   public static void main(String[] args) {
     int zahl;
     try {
       zahl = Integer.parseInt("s");
     } catch (Throwable t) {
       t.printStackTrace();
       System.out.println("\nSetze Wert auf 4!");
       zahl = 4;
     }
     System.out.println("Quadrat unserer Zahl: " + (zahl*zahl));
   }
}

nach oben

2. mehrere catch-Blöcke

Die Aufgabe des Quadrierens einer Zahl, die vom Benutzer eingegeben wird, hatten wir im letzten Kapitel schon gelöst. Dabei hatten wir zwei mögliche Ausnahmen zu beachten: IOException (wegen evtl. Fehler beim Einlesen) und NumberFormatException (für Fehler beim Umwandeln in eine Ganzzahl). Dieses Beispiel will ich nochmal aufgreifen und anhand dessen das Konstrukt mehrerer catch-Blöcke einführen:

import java.io.*;

public class FehlerTest8 {
   public static int quadrat(String input) 
              throws NumberFormatException {
       int zahl = Integer.parseInt(input);
       return zahl*zahl;
   }
   public static void main(String[] args) {
     try {
       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Bitte gib eine Zahl ein: ");
       String input = in.readLine();
       int ergebnis = quadrat(input);
       System.out.println("Quadrat: " + ergebnis);
     } catch (NumberFormatException nfe) {
       System.out.println("Eingabe ist keine gueltige Zahl, Quadrat kann nicht berechnet werden!");
     } catch (IOException ioe) {
       System.out.println("Art des Fehlers: " + ioe.toString());
       ioe.printStackTrace();
     }
   }
}

Wie man sieht, braucht man die Fehler (logischerweise) nicht mehr mit throws weiterreichen, wenn man sie selbst behandelt. Mehrere verschiedene Ausnahmen abzufangen ist denkbar einfach: man reiht lediglich beliebig viele catch-Blöcke hintereinander. Jeder catch-Block behandelt einen anderen Fehler und führt entsprechenden Code aus, je nachdem welche Ausnahme im try-Block tatsächlich aufgetreten ist.
Interessant ist noch, dass in der "main"-Methode selbst wieder keine Anweisung vorhanden ist, die eine NumberFormatException kann. Oder sollte ich besser sagen: scheinbar. Denn die Methode "quadrat" reicht eine NumberFormatException an den Aufrufer weiter, hier die Methode "main". Diese muss dann erneut entscheiden, wie sie den Fehler behandeln will. Im vorigen Kapitel haben wir uns für "throws" entschieden, dieses Mal jedoch für "catch" (catch-or-throw-Regel).
Es ist wohl unnötig zu sagen, dass man natürlich auch mehrere try-catch-Blöcke ineinander schachteln kann, ähnlich den bedingten Ausdrücken und Schleifen. So können, innerhalb eines try-Blocks, ruhig weitere try-catch-Blöcke stehen.

nach oben

3. der finally-Block

Während die Fehlerbehandlung mit try-catch auch in anderen Programmiersprachen wie C++ zu finden ist, bietet Java hier noch etwas mehr. Wenn man Programmcode unabhängig davon ausführen lassen will, ob eine Ausnahme auftritt oder nicht, so müsste man bis jetzt den selben Quelltext zweimal schreiben: einmal im try-Block und einmal im catch-Block. Der finally-Block in Java hingegen erlaubt es, Anweisungen zu deklarieren, die in jedem Fall ausgeführt werden sollen. Er wir einfach an den letzten catch-Block angehängt:

import java.io.*;

public class FehlerTest9 {
   public static int quadrat(String input) 
              throws NumberFormatException {
       int zahl = Integer.parseInt(input);
       return zahl*zahl;
   }
   public static void main(String[] args) {
     try {
       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Bitte gib eine Zahl ein: ");
       String input = in.readLine();
       int ergebnis = quadrat(input);
       System.out.println("Quadrat: " + ergebnis);
     } catch (NumberFormatException nfe) {
       System.out.println("Eingabe ist keine g&#252;ltige Zahl, Quadrat kann nicht berechnet werden!");
     } catch (IOException ioe) {
       System.out.println("Art des Fehlers: " + ioe.toString());
       ioe.printStackTrace();
     } finally {
       System.out.println("Programm wird beendet!");
     }
   }
}

Das besondere am finally-Block ist, dass er in jedem Fall ausgeführt wird, sobald das Programm den try-Block betritt, egal ob eine Ausnahme ausgelöst wurde oder nicht. Mehr noch, er wird auch dann ausgeführt, wenn der gesamte Block vorzeitig verlassen werden soll, z.B. durch eine "break"- oder "return"-Anweisung (innerhalb einer Methode) oder das Programm im Fehlerfall abgebrochen wird. In diesem Fall sollten die gewünschten Anweisungen nicht einfach hinter die try-catch-Blöcke gestellt werden, sondern in einen finally-Block gepackt werden. Üblicherweise werden deshalb im finally-Block Ein- und Ausgabeströme (z.B. für Dateien), abschließende Meldungen etc. untergebracht.

nach oben

4. Übung

Schreibe das Programm "FehlerTest4" aus dem letzten Kapitel so um, dass es die Ausnahmen nicht weiterreicht, sondern abfängt und behandelt!

nach oben

5. Lösung

import java.io.*;

public class FehlerTest4 {

  public static double hektarNachAr(double flaeche)
                 throws ArithmeticException {
    if (flaeche < 0) {
        ArithmeticException ae = new ArithmeticException("Flaeche < 0");
        throw ae;
    }
    return flaeche*100;
  }

  public static void main(String[] args) {
    try {
      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      System.out.print("Bitte gib die Flaeche in Hektar ein: ");
      double ha = Double.parseDouble(in.readLine());
      double ar = hektarNachAr(ha);
      System.out.println("Flaeche in Hektar: " + ar);
    } catch (ArithmeticException ae) {
      System.out.println("Fehler: " + ae);
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }

}

nach oben