Behandlung von Ausnahmen (Deklaration)

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

  1. Was sind Ausnahmen?
  2. Ausnahmen weiterreichen
  3. kontrollierte Fehler vs. Laufzeitfehler
  4. Ausnahmen auslösen

1. Was sind Ausnahmen?

In den vorherigen Kapiteln fiel bereits des öfteren der Begriff Ausnahme bzw. Exception. Nun gehen wir darauf näher ein. Bei einer Exception handelt es sich um einen Programmfehler, der vom Java-Programm noch kontrolliert werden kann und nicht zum Programmabbruch führen muss. Dagegen stellt ein Error einen schweren Fehler dar, der vom Programm nicht mehr kontrolliert werden kann. Logischerweise sehen wir uns die kontrollierbaren "Exceptions" an. Das Interessante dabei ist, dass zu jeder auftretenden Ausnahme eine korrespondierende Klasse existiert und damit auch Objekte und Methoden. Was damit genau gemeint ist, werde ich noch näher erläutern. Beginnen wir zunächst mit folgendem kleinen Quelltext:

import java.io.*;

public class FehlerTest1 {
   public static void main(String[] args) {
       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Bitte gib Deinen Namen ein: ");
       String name = in.readLine();
       System.out.println("Hallo " + name);
   }
}

Zunächst wird das Paket java.io importiert. Es enthält Klassen für Ein- und Ausgabeströme ("Input-Streams" bzw. "Output-Streams"), "io" steht demzufolge als Abkürzung für "input" und "output". Diese Klassen sind notwendig, wenn man mehr will als nur Text auf der Konsole ausgeben, z.B. Eingaben von Konsole oder Dateien etc. einlesen. Das auf den ersten Blick seltsame Konstrukt um das "BufferedReader"-Objekt wird später erklärt, zunächst übernehmen wir das einfach so und merken uns, dass damit ein gepufferter Eingabestrom erzeugt wird. Über das erzeugte BufferedReader-Objekt, das ich hier "in" genannt habe, kann mit der Methode "readLine" das Programm angehalten werden, damit es auf eine Benutzereingabe von Tastatur wartet (mit Return wird die Eingabe abgeschlossen).
Wollte man diese Klasse nun kompilieren, würde der Compiler mal wieder rummotzen, und zwar mit folgendem Text (unter Linux):

screenshot

Wenn man die Fehlermeldung richtig liest (die Übung kriegt man mit der Zeit), bekommt man vom Compiler eine schone Beschreibung dessen, was man falsch gemacht hat. "unreported exception" bedeutet, dass wir hier einen Programmteil haben, das Ausnahmen erzeugen kann. Netterweise teilt uns der Compiler auch gleich mit, was für eine Ausnahme hier auftreten kann: java.io.IOException. Das bedeutet, dass in diesem Programm eine "IOException" auftreten kann, deren Klasse sich ebenfalls im Paket "java.io" befindet. Was man dagegen machen kann, verräte der Compiler ebenfalls: "must be caught or declared to be thrown".
Damit sehen wir bereits, dass es zwei Möglichkeiten gibt: entweder fangen wir eine Ausnahme ab ("catch"), oder deklarieren sie zum Weiterreichen an die aufrufende Stelle ("throws"). Diese beiden Möglichkeiten ergeben die Grundregel der Fehlerbehandlung in Java (catch-or-throw-Regel): entweder abfangen und behandeln oder aber deklarieren und weiterreichen.

nach oben

2. Ausnahmen weiterreichen

In diesem Kapitel wenden wir uns zunächst dem Weiterreichen von Ausnahmen zu. Das ist insofern recht einfach, als dass wir dabei noch nicht direkt mit der Objekteigenschaft von Ausnahmen in Berührung kommen, sondern nur den Typ der Ausnahme (d.h. den Namen der zugehörigen Klasse) angeben müssen.
Dies ist recht einfach: wird innerhalb einer Methode eine (potenzielle) Ausnahme erzeugt, so schreibt man hinter den Methodenkopf, aber noch vor der öffnenden, geschweiften Klammer das Schlüsselwort throws gefolgt von einer Auflistung aller in dieser Methode möglichen Ausnahmen (durch Kommata getrennt). In unserem obigen Beispiel erzeugt die Methode "readLine" des BufferedReader-Objekts potenziell eine Ausnahme "IOException" (generell sind Benutzereingaben fehlerbehaftet). Wir müssten also unser Programm so umschreiben:

import java.io.*;

public class FehlerTest1 {
   public static void main(String[] args) throws IOException {
       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Bitte gib Deinen Namen ein: ");
       String name = in.readLine();
       System.out.println("Hallo " + name);
   }
}

Jetzt ist auch der Compiler zufrieden. Da die Ausnahme innerhalb der Methode "main" auftreten kann, muss diese Methode die Ausnahmen zum Weiterreichen deklarieren. Diese Ausnahmen werden jetzt zwar nicht explizit behandelt, allerdings wurden sie dem Compiler nun bekannt gemacht, so dass er überwachen kann, dass der Aufrufer seinerseits die Ausnahme entweder deklariert oder weiterreicht.
Was ist mit "Aufrufer" gemeint? Nehmen wir als Beispiel die statische Methode "parseInt" aus der Hüllklasse "Integer": ein Blick in die Java-Doku verrät uns, dass diese Methode eine "NumberFormatException" erzeugen kann und diese mit "throws" weiterreicht. Zur Wiederholung:

public static int parseInt(String s) throws NumberFormatException

Verwenden wir diese Methode nun in einem kleinen Programm:

import java.io.*;

public class FehlerTest2 {
   public static int quadrat(String input) 
              throws NumberFormatException {
       int zahl = Integer.parseInt(input);
       return zahl*zahl;
   }
   public static void main(String[] args) 
              throws IOException, NumberFormatException {
       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);
   }
}

Da die Methode "parseInt" innerhalb der Methode "quadrat" aufgetreten ist, ist "quadrat" der Aufrufer und muss nun ihrerseits entscheiden, wie die Ausnahme zu behandeln ist. Hier wird die Ausnahme mit "throws" wiederum weitergegeben. Die Methode "quadrat" wird von der Methode "main" aus aufgerufen und gibt an sie die "NumberFormatException" weiter. Das bedeutet, dass nun "main" die Ausnahme weiter behandeln soll und deshalb ebenfalls diese Ausnahme deklarieren soll, obwohl sie zuerst durch "parseInt" in "quadrat" ausgelost wurde. Man muss aufpassen, dass man dabei den überblick behält, aber mit etwas Übung kommt das von alleine. Übrigens kann man die "throws"-Deklaration ruhig (aus Lesbarkeitsgründen) in eine neue Zeile schreiben.

nach oben

3. kontrollierte Fehler vs. Laufzeitfehler

Bleibt für den Moment noch eine Frage zu klären: warum ist die Behandlung einer "IOExcpetion" obligatorisch, während man bei einer "NumberFormatException" darauf verzichten kann (siehe Quelltexte im vorherigen Kapitel), ohne dass der Compiler meckert? Das liegt daran, dass man die Ausnahmen nochmal in zwei Untergruppen unterteilen kann: kontrollierte Fehler ("checked exceptions") und Laufzeitfehler ("runtime-exceptions"). Eine IOException gehört zu den kontrollierten Fehlern und muss daher behandelt werden, da Ein- und Ausgabeoperationen potenziell immer Fehler erzeugen können. Die "NumberFormatException" gehört dagegen zu den Laufzeitfehlern. Dies deshalb, weil erst zur Laufzeit bekannt ist, was denn die Variable "input" aufgrund der Benutzereingabe enthält. Laufzeitfehler müssen deshalb nicht deklariert werden.

Klassen und Objekte sowie deren Vererbung sind erst Gegenstand eines späteren Kapitels, trotzdem soll aus systematischen Gründen bereits jetzt auf die Hierarchie der Ausnahmeklassen eingegangen werden.
Die Mutterklasse der Ausnahmen (oder "Ursprung allen übels", wie ich es gerne nenne) bildet die Klasse Throwable. Von ihr gehen zwei Vererbungslinien aus: die unkontrollierbaren Errors und die kontrollierbaren Exceptions. Wir sehen uns den zweiten Zweig an, an deren Spitze sich die Klasse Exception befindet. Unter ihr teilen sich wieder zwei größere Zweige auf:

Die kontrollierten Ausnahmen wie die "IOExcpetion" sind direkt aus der Klasse "Exception" im Paket "java.lang" abgeleitet, während alle Laufzeitfehler aus der Klasse "RuntimeException" (ebenfalls im Paket "java.lang") abgeleitet sind. (An der Spitze aller Vererbungshierarchien steht immer die Klasse "Object", aus ihr sind ausnahmslos alle Klassen, die es in Java gibt, abgeleitet, aber dazu später mehr)
Ordnen wir nun alle bisher bekannten Ausnahmen zu:

  1. kontrollierte Fehler:
  2. Laufzeitfehler:

Das Interessante daran ist, dass jede spezielle Ausnahme, egal ob "IOException", "IndexOutOfBoundsException" usw., gleichzeitig auch vom Typ "Exception" bzw. "Throwable" ist (da diese die Elternklassen aller Ausnahmen bilden). Das bedeutet, dass man, wenn man die spezielle Fehlerklasse nicht kennt, man sich damit behelfen kann, lediglich eine Ausnahme vom Typ "Exception" weiterzureichen:

import java.io.*;

public class FehlerTest3 {
   public static void main(String[] args) 
                  throws Exception {
       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Bitte gib Deinen Namen ein: ");
       String name = in.readLine();
       System.out.println("Hallo " + name);
   }
}

Im Sinne einer besseren Kontrolle durch den Programmierer sollten aber normalerweise die Ausnahmen schon genau benannt werden. Ein Blick in die Java-Doku kann hier helfen und zeigt auch die Elternklassen der Ausnahmen an, anhand derer man auch zwischen kontrollierten Ausnahmen und Laufzeitfehlern unterscheiden kann.

nach oben

4. Ausnahmen auslösen

Da Ausnahmen in Java in Klassen gekapselt werden, ist es dem Programmierer möglich, eigene Ausnahme-Objekte zu erzeugen. Nehmen wir einmal, wir wollten eine Methode schreiben, die eine Flächenangabe von Ar in Hektar umrechnet. Da eine Fläche per Definition nicht kleiner als 0 sein kann, soll eine ArithmeticException ausgelöst werden:

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)
                 throws IOException, ArithmeticException {
    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 = arNachHektar(ha);
    System.out.println("Flaeche in Hektar: " + ar);
  }

}

In der Methode "hektarNachAr" wird zunächst überprüft, ob die Fläche negativ ist. Ist dies der Fall, wird eine ArithmeticException ausgelöst. Ein entsprechendes Objekt wird - wie üblich - über das Schlüsselwort new erzeugt. Dem Konstruktor kann optional ein String übergeben werden, der die Ursache für die Ausnahme angibt.
Um die Ausnahme letztendlich auszulösen, benötigt man eine throw-Anweisung. Syntax:

throw AusnahmeObjekt;

Dadurch wirft diese Methode potentiell eine Ausnahme aus, weswegen sie auch die catch-or-throw-Regel beachten muss. Das bedeutet, die Methode kann die Ausnahme gleich selbst abfangen, viel öfter wird man jedoch die Ausnahme an den Aufrufer weiterleiten. Dies deshalb, weil das Auslösen von Ausnahmen vor allem in Methoden Sinn macht, die auch von anderen Programmierern benutzt werden. Wenn diese dann die Methode verwenden, sollen sie selbst entscheiden, wie sie mit der Ausnahme verfahren.
Wird also in obigem Quelltext eine Ausnahme vom Typ ArithmeticException ausgelöst, so muss die Methode "hektarNachAr" ebenfalls mit throws deklariert werden, um die Ausnahme weiterzureichen. Der Aufrufer - die Methode "main" - muss dann wieder nach der catch-or-throw-Regel entscheiden, was mit der Ausnahme geschehen soll.
Die Methode "hektarNachAr" kann noch etwas gekürzt werden, in dem man das Ausnahme-Objekt sofort nach dem Erzeugen an throw weitergibt:

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

nach oben