8.3 Auslösen eigener Exceptions 

Bisher wurden Exceptions lediglich aufgefangen, aber noch nicht selbst erzeugt. In diesem Kapitel wollen wir sehen, wie eigene Ausnahmen ausgelöst werden. Das kann zum einen erfolgen, wenn die JVM provoziert wird, etwa bei einer ganzzahligen Division durch 0, oder explizit durch throw.
8.3.1 Mit throw Ausnahmen auslösen 

Soll eine Funktion selbst eine Exception auslösen, muss sie zunächst ein Exception-Objekt erzeugen und dann die Ausnahmebehandlung anstoßen. Im Sprachschatz dient das Schlüsselwort throw dazu, eine Ausnahme auszulösen. Als Exception-Typ soll IllegalArgumentException dienen, das ein fehlerhaftes Argument anzeigt.
Listing 8.11 com/tutego/insel/exceptions/v1/Player.java, Konstruktor
Player( int age ) { if ( age <= 0 ) throw new IllegalArgumentException( "No age <= 0 allowed!" ); this.age = age; }
Wir sehen im Beispiel, dass negative oder null Alter-Übergaben nicht gestattet sind und zu einem Fehler führen. Im ersten Schritt baut dazu der new-Operator das Exception-Objekt über einen parametrisierten Konstruktor auf. Die Klasse IllegalArgumentException bietet einen solchen Konstruktor, der eine Zeichenkette annimmt, die den näheren Grund der Ausnahme übermittelt. Welche Parameter die einzelnen Exception-Klassen deklarieren, ist der API zu entnehmen. Nach dem Aufbau des Exception-Objekts beendet throw die lokale Abarbeitung, und die JVM sucht ein catch, das die Ausnahme behandelt.
Lassen wir ein Beispiel folgen, in dem Spieler mit einem negativen Alter initialisiert werden sollen:
Listing 8.12 com/tutego/insel/exceptions/v1/Player.java, main()
try
{
Player d = new Player( –100 );
System.out.println( d );
}
catch ( IllegalArgumentException e )
{
e.printStackTrace();
}
Das führt zu einer Exception, und der Stack-Trace, der den printStackTrace() ausgibt, ist:
java.lang.IllegalArgumentException: No age <= 0 allowed! at com.tutego.insel.exceptions.v1.Player.<init>(Player.java:10) at com.tutego.insel.exceptions.v1.Player.main(Player.java:19)
Player p = null;
try { p = new Player(v); } catch ( IllegalArgumentException e ) { }
p.getAge(); // (*) BUM! Die Exception führt zu keinem Player-Objekt, wenn v negativ ist. So bleibt p mit null vorbelegt. Es folgt in der BUM-Zeile eine NullPointerException. Der Programmcode, der das Objekt erwartet, sollte also mit in den try-catch-Block. |
Da die IllegalArgumentException eine RuntimeException ist, hätte es in main() auch ohne try-catch heißen können:
public static void main( String[] args ) { Player d = new Player( –100 ); }
Die Runtime-Exception müsste nicht zwingend aufgefangen werden, aber der Effekt wäre, dass die Ausnahme nicht behandelt würde und das Programm abbricht.
class java.lang.IllegalArgumentException
extends RuntimeException |
- IllegalArgumentException() Erzeugt eine neue Ausnahme ohne genauere Fehlerangabe.
- IllegalArgumentException( String s ) Erzeugt ein neues Fehler-Objekt mit einer detaillierteren Fehlerangabe.
8.3.2 Neue Exception-Klassen deklarieren 

Eigene Exceptions sind immer direkte (oder indirekte) Unterklassen von Exception. Sie implementieren oft zwei Konstruktoren: einen Standard-Konstruktor und einen mit einem String parametrisierten Konstruktor. Um für die Klasse Player im letzten Beispiel einen neuen Fehlertyp zu deklarieren, erweitern wir RuntimeException zur PlayerException.
Listing 8.13 com/tutego/insel/exception/v2/PlayerException.java
package com.tutego.insel.exception.v2;
public class PlayerException extends RuntimeException
{
public PlayerException()
{
}
public PlayerException( String s )
{
super( s );
}
}
Nehmen wir uns die Initialisierung mit dem Alter noch einmal vor. Statt der IllegalArgument-Exception löst der Konstruktor im Fehlerfall unsere speziellere PlayerException aus.
Listing 8.14 com/tutego/insel/exception/v2/Player.java, Ausschnitt
if ( age <= 0 )
throw new PlayerException( "No age <= 0 allowed!" );
Im Hauptprogramm können wir auf die PlayerException reagieren, indem wir die Ausnahme explizit mit try-catch auffangen oder an den Aufrufer weitergeben – unsere Exception ist ja eine RuntimeException und müsste nicht direkt abgefangen werden.
Exception in thread "main" com.tutego.insel.exceptions.v2.PlayerException: No age <= 0 allowed! at com.tutego.insel.exceptions.v2.Player.<init>(Player.java:10) at com.tutego.insel.exceptions.v2.Player.main(Player.java:19)
|
8.3.3 Abfangen und Weiterleiten 

Die Ausnahme, die ein try-catch-Block auffängt, kann in catch wieder neu ausgelöst werden. Dieses Re-throwing zeigt folgendes Beispiel, in dem das Programm den Fehler erst über einen Logger ausgibt und dann weiter nach oben reicht.
Listing 8.15 Rethrow.java
import java.util.logging.Logger; public class Rethrow { private static Logger log = Logger.getAnonymousLogger(); static void rethrow( RuntimeException e ) { log.warning( "RuntimeException occurred!" ); // e.fillInStackTrace(); throw e; } public static void main( String[] args ) { try { ((String)null).length(); } catch ( NullPointerException e ) { rethrow( e ); } } }
Die Ausgabe ist:
16.08.2006 18:35:27 Rethrow rethrow WARNUNG: RuntimeException occurred! Exception in thread "main" java.lang.NullPointerException at Rethrow.main(Rethrow.java:20)
Wichtig an dieser Stelle ist die Tatsache, dass der Aufrufstack in e gespeichert ist und daher der Strack-Trace nicht die Funktion rethrow() enthält. Wünschten wir dies, müssen wir den Strack-Trace neu füllen. Dazu dient die Funktion fillInStackTrace(). Nehmen wir den Aufruf im oberen Beispiel hinein, folgt die Ausgabe:
16.08.2006 18:36:16 Rethrow rethrow
WARNUNG: RuntimeException occurred!
Exception in thread "main" java.lang.NullPointerException
at Rethrow.rethrow(Rethrow.java:11)
at Rethrow.main(Rethrow.java:24)
8.3.4 Geschachtelte Ausnahmen 

Besonders bei eigenen Ausnahmen mag ein Grund für die Auslösung sein, dass ein eingebetteter Teil versagt. Das ist vergleichbar mit einer Transaktion: Ist ein Teil der Kette fehlerhaft, so ist der ganze Teil nicht ausführbar. Bei unseren Ausnahmen ist das nicht anders. Nehmen wir an, wir haben eine Methode, die im Falle eines Misslingens eine Ausnahme auslöst. Ruft unsere Methode nun eine Unterfunktion auf, die zum Beispiel eine Ein-/Ausgabeoperation tätigt, was jedoch schiefgeht, wird der Anlass für unsere Exception die IO-Exception sein. Es liegt also nahe, bei der Nennung des Grundes für das eigene Versagen die Unteraufgabe zu nennen. (Wieder ein Beweis, wie »menschlich« Programmieren sein kann.)
Eine geschachtelte Ausnahme (engl. nested exception) speichert einen Verweis auf eine weitere Ausnahme. Wenn ein Exception-Objekt aufgebaut wird, lässt sich der Grund (engl. cause) als Argument im Konstruktor der Throwable-Klasse übergeben. Die Ausnahme-Basisklasse bietet dafür zwei Konstruktoren:
- Throwable( Throwable cause )
- Throwable( String message, Throwable cause )
Der Grund kann über die Methode Throwable getCause() erfragt werden.
Da Konstruktoren in Java nicht vererbt werden, bieten die Unterklassen oft Konstruktoren an, um den Grund anzunehmen: Zumindest Exception macht das und kommt somit auf vier Erzeuger:
- Exception()
- Exception( String message )
- Exception( String message, Throwable cause )
- Exception( Throwable cause )
Einige der tiefer liegenden Unterklassen haben dann auch diese Konstruktor-Typen mit Throw-able-Parameter, wie IOException, SQLException oder ClassNotFoundException, andere wiederum nicht, wie PrinterException. Eigene Unterklassen können auch mit initCause() genau einmal eine geschachtelte Ausnahme angeben.
In modernen Frameworks ist die Nutzung von Ausnahmen, die nicht geprüft werden müssen, also Exemplare von RuntimeException sind, häufiger geworden. Bekannte zu prüfende Ausnahmen werden in RuntimeException-Objekte verpackt (eine Art Exception-Wrapper), die den Verweis auf die auslösende Nicht-RuntimeException speichern.
8.3.5 Rückgabewerte bei ausgelösten Ausnahmen 

Java versucht, durch den Programmfluss den Ablauf innerhalb einer Methode zu bestimmen und zu melden, ob sie definitiv einen Rückgabewert liefert. Dabei verfolgt der Compiler die Programmpfade und wertet bestimmte Ausdrücke aus. Doch die Aussage »Jede Funktion mit Ergebnistyp ungleich void muss eine return-Anweisung besitzen« müssen wir etwas relativieren. Nur in einem speziellen Fall müssen wir dies nicht. Nämlich genau dann, wenn vor dem Ende der Funktion eine throws-Anweisung die Abarbeitung beendet.
Listing 8.16 com/tutego/insel/exceptions/NoReturn.java
package com.tutego.insel.exception; class WindowsVistaKeyGenerator { public String generateKey() { throw new UnsupportedOperationException(); } }
Ein Blick auf generateKey() verrät, dass trotz angekündigtem Rückgabewert keine return-Anweisung im Rumpf steht. Die Abarbeitung wird vor dem Rücksprung durch eine Exception abgebrochen. generateKey() muss diese Exception nicht mit throws ankündigen, da UnsupportedOperationException eine RuntimeException ist.