Exceptions
Der primäre Mechanismus, der Java zur Verfügung stellt um Fehlersituationen anzuzeigen und zu behandeln sind Exceptions. Exceptions sind Objekte, die Informationen zum aufgetretenen Fehler enthalten. Wir sagen, dass Exceptions an der Stelle, an welcher der Fehler auftritt, geworfen werden. Java unterbricht die Ausführung der aktuellen Methode und gibt die Kontrolle an die aufrufende Methode zurück. Falls diese den aufgetretenen Fehler fängt, also Code zur Fehlerbehandlung zur Verfügung stellt, wird dieser Fehlerbehandlungscode ausgeführt. Ansonsten geht die Kontrolle zum nächsten Aufrufer. Dies geht so lange weiter, bis eine Fehlerbehandlungsroutine gefunden wird oder die Main-Methode erreicht ist. In diesem Fall bricht das Programm ab.
Diese nachfolgende Illustration zeigt den Unterschied zwischen dem normalen Ablauf (Reihe 1) und einem Ablauf indem ein Fehler auftritt.
In den nachfolgenden Abschnitten erklären wir die Details dieses Mechanismus.
Exceptions definieren
Exceptions sind einfach Klassen, welche von der Klasse Exception
erben.
Um einen bestimmten Fehler anzuzeigen, definieren wir eine entsprechende Subklasse.
In Java sind auch bereits viele solcher Klassen vordefiniert.
So gibt es
die IOException,
welche Fehler bei Ein- und Ausgabe anzeigt,
die NullPointerException,
die einen Zugriff auf einen Null-Wert anzeigt oder
die IndexOutOfBoundsException,
die anzeigt, dass in einem Array auf ein Element zugegriffen wurde, welches im Array nicht existiert.
Java bietet unterschiedliche Exceptions an, es kann jedoch vorkommen, dass wir einen neuen Fehlerfall anzeigen möchten. Dann können wir natürlich auch eine eigene Exceptions definieren. Dies ist nachfolgend Illustriert:
class MyException extends Exception {
MyException(String errorMessage) {
super(errorMessage);
}
}
Im Konstruktor müssen wir jeweils den Superklassenkonstruktor (also den Konstruktor der Klasse Exception
) aufrufen.
Dieser nimmt als Argument einen String entgegen, der die Fehlermeldung enthält.
Exceptions werfen
Wenn in unserem Code eine Fehlersitutation auftritt, erzeugen wir eine Instanz einer entsprechenden Exceptionklasse.
Danach werfen wir diese mit dem Schlüsselwort throw
.
Dies ist in folgendem Beispiel illustriert:
if (unexpectedSituation) {
MyException myException = new MyException("something unexpected happens");
throw myException;
}
Wenn wir in einer Methode eine Exception werfen, müssen wir dies zusätzlich noch in der Methodendeklaration deklarieren.
Dies geschieht mit der throws
Klausel.
Nach dem Schlüsselwort throws
Listen wir einfach (durch Komma getrennt) alle Exceptionklassen auf, die geworfen werden können.
In unserem Beispiel ist dies nur die Klasse MyException
:
void exampleMethod() throws MyException {
// ...
if (unexpectedSituation) {
MyException myException = new MyException("something unexpected happens");
throw myException;
}
// ...
}
Exceptions fangen
Wir haben nun gesehen, dass innerhalb von Methoden Exceptions geworfen werden können. Wenn immer wir in einer Methode, eine andere Methode aufrufen, die eine Exception werfen kann, müssen wir diese behandeln. Dies kann auf zwei verschiedene Arten geschehen:
- Wir machen nichts und propagieren die Exception weiter.
- Wir fangen die Exception und führen eine entsprechende Fehlerbehandlung durch.
Im ersten Fall reicht unsere Methode die Exception einfach weiter an deren Aufrufer.
Das heisst, in unserer Methoden kann auch ein Fehler auftreten.
Entsprechend müssen wir dies wieder durch throws
in der Methodensignatur kennzeichnen.
Im zweiten Fall, müssen wir die Exception fangen. Dies geschieht mit folgender Syntax:
try { // Start des geschützten Bereichts
// ...
exampleMethod() // hier könnte ein Fehler auftreten
// ...
} catch (Exception e) {
// Fehlerbehandlung
}
Dabei wird der Teil des Codes, welcher eine Exception werfen könnte mit einem try
-Block umschlossen.
Im Beispiel ist dies der Aufruf von exampleMethod()
.
Die darauf folgende catch
-Klausel gibt an, welche Exception gefangen wird.
Wird eine Exception gefangen, wird diese der Variable zugewiesen, hier e
.
Die Fehlerbehandlung erfolgt dann im catch
-Block, wo auf die Variable e
zugegriffen werden kann.
Am besten Sie experimentieren mit diesem Konzept gleich selbst.
Experimente
- Was passiert, wenn Sie in der
catch
Klausel stattMyException
Exception
schreiben? - Was passiert, wenn Sie in der
catch
Klausel stattMyException
IOException
schreiben? - Was passiert, wenn Sie die Exception in a
aMethodThatCatches
nicht fangen? Wie müssen Sie die Methode dann anpassen? - Können Sie mehrere
catch
Klauseln an einentry
-Block anhängen um unterschiedliche Fehler zu fangen und behandeln?
Die finally Klausel
Neben try
und catch
gibt es noch einen weitere speziellen Block, den wir an den Fehlerbehandlungscode anfügen können.
Dies ist der finally
Block.
Dieser Block wird immer aufgerufen, egal ob eine Exception geworfen wurde oder nicht.
Die vollständige Fehlerbehandlung sieht dann so aus.
try { // Start des geschützten Bereichts
// ...
exampleMethod() // hier könnte ein Fehler auftreten
// ...
} catch (Exception e) {
// Fehlerbehandlung
} finally {
// Code der immer ausgeführt werden muss
}
Eine typische Anwendung der finally Klausel ist, diese für Aufräumarbeiten zu nutzen. Wenn eine Methode gewisse Ressourcen des Systems, wie zum Beispiel Dateien, nutzt, müssen diese am Ende wieder freigegeben werden. Das soll sowohl im Fehlerfall als auch im Normalfall geschehen. Dank der finally-Klausel müssen wir diese nicht zweimal schreiben. Zudem garantiert Java sogar, dass der Code in der finally Klausel ausgeführt wird, auch wenn in der Catch-Klausel wieder ein Fehler auftritt.
Experimente
- Was passiert, wenn Sie in der catch-Klausel wieder eine Exception werfen? Wird die finally Klausel noch aufgerufen?
Runtime exceptions
In Java gilt im allgemeinen die Regel, dass jede Exception, die in einer Methode geworfen werden kann, in der Methodensignatur mittels throws
deklariert werden muss.
Eine Ausnahme besteht für RuntimeExceptions
und Exceptions die davon abgeleitet sind.
Wenn eine RuntimeException
geworfen wird, dann muss diese nicht deklariert werden.
RuntimeExceptions
werden meist für Fehler eingesetzt, die überall im Programm auftreten können.
Zudem sind dies normalerweise Fehler auf die man nicht einfach so reagieren kann.
Somit wäre es unpraktisch, wenn wir diese überall deklarieren müssten.
Ein Beispiel eines solchen Fehlers ist die VMOutOfMemoryException
.
Diese zeigt an, dass zu wenig Speicher für die Ausführung des Programms zur Verfügung steht.
Diese Art der Exception wird aber häufig auch missbraucht.
Java-Programmierer:innen tendieren dazu eigene Exceptions als RuntimeExceptions
zu deklarieren.
Der Grund dafür ist, dass das Behandeln aller Fehlerfälle manchmal etwas mühsam sein kann.
Dies ist aber eine umstrittene Praxis, die eher der Bequemlichkeit geschuldet ist als guter Programmierpraxis.
Wir wollen dies in diesem Kurs nicht anwenden und es ist auch nicht im Sinne der Designer der Sprache.
Haben Sie Fragen oder Bemerkungen? Schreiben Sie diese doch ins Forum.