8.6 Auslösen eigener Exceptions
Bisher wurden Exceptions lediglich aufgefangen, aber noch nicht selbst erzeugt. In diesem Abschnitt 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.6.1 Mit throw Ausnahmen auslösen
Soll eine Methode oder ein Konstruktor selbst eine Exception auslösen, muss zunächst ein Exception-Objekt erzeugt und dann die Ausnahmebehandlung angestoßen werden. Im Sprachschatz dient das Schlüsselwort throw dazu, eine Ausnahme zu signalisieren und die Abarbeitung an der Stelle zu beenden.
Als Exception-Typ soll im folgenden Beispiel IllegalArgumentException dienen, das ein fehlerhaftes Argument anzeigt:
Player( int age ) {
if ( age <= 0 )
throw new IllegalArgumentException( "Kein Alter <= 0 erlaubt!" );
this.age = age;
}
Wir sehen im Beispiel, dass negative Alter-Übergaben oder solche mit 0 nicht gestattet sind und zu einer Ausnahme führen. Im ersten Schritt baut dazu new 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.
[»] Hinweis
Ein throws IllegalArgumentException am Konstruktor ist in diesem Beispiel überflüssig, da IllegalArgumentException eine RuntimeException ist, die nicht über ein throws in der Methoden-Signatur angegeben werden muss.
Lassen wir ein Beispiel folgen, in dem Spieler mit einem negativen Alter initialisiert werden sollen:
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, den printStackTrace() ausgibt, ist:
Exception in thread "main" java.lang.IllegalArgumentException: Kein Alter <= 0
erlaubt!
at com.tutego.insel.exception.v1.Player.<init>(Player.java:9)
at com.tutego.insel.exception.v1.Player.main(Player.java:28)
[»] Hinweis
Löst ein Konstruktor eine Ausnahme aus, ist eine Nutzung wie die folgende problematisch:
Player p = null;
try {
p = new Player( v );
}
catch ( IllegalArgumentException e ) { }
p.getAge(); // BUMM: NullPointerException
Die Exception führt zu keinem Player-Objekt, wenn v negativ ist. So bleibt p mit null vorbelegt. Es folgt in der BUMM-Zeile eine NullPointerException. Der Programmcode, der das Objekt erwartet, aber vielleicht mit einer null rechnet, sollte mit in den try-Block. Doch üblicherweise stehen solche Ausnahmen für Programmierfehler und werden nicht aufgefangen.
Da die IllegalArgumentException eine RuntimeException ist, hätte es in main() auch ohne try-catch so 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 abbräche.
class java.lang.IllegalArgumentException
extends RuntimeException
IllegalArgumentException()
Erzeugt eine neue Ausnahme ohne genauere Fehlerangabe.IllegalArgumentException(String s)
Erzeugt ein neues Ausnahmeobjekt mit einer detaillierten Fehlerangabe.
8.6.2 Vorhandene Runtime-Ausnahmetypen kennen und nutzen
Die Java-API bietet eine große Anzahl von Exception-Klassen, und so muss nicht für jeden Fall eine eigene Exception-Klasse deklariert werden. Viele Standardfälle, wie falsche Argumente oder falscher Programmstatus, decken Standard-Exception-Klassen ab.
[»] Hinweis
Entwickler sollten nie throw new Exception() oder sogar throw new Throwable() schreiben, sondern sich immer konkreter Unterklassen bedienen.
Einige Standard-Runtime-Exception-Unterklassen des java.lang-Pakets folgen nun in einer Übersicht.
IllegalArgumentException
Die IllegalArgumentException zeigt an, dass ein Parameter nicht korrekt angegeben ist. Dieser Ausnahmetyp lässt sich somit nur bei Konstruktoren oder Methoden ausmachen, denen fehlerhafte Argumente übergeben wurden. Oft ist der Grund die Missachtung des Wertebereichs. Wenn die Werte grundsätzlich korrekt sind, darf dieser Ausnahmetyp nicht ausgelöst werden. Dazu folgen in Abschnitt 8.6.3 noch ein paar mehr Details.
IllegalStateException
Objekte haben in der Regel Zustände. Gilt es, Operationen auszuführen, sind aber die Zustände nicht korrekt, so kann die Methode eine IllegalStateException auslösen und so anzeigen, dass in dem aktuellen Zustand die Operation nicht möglich ist. Wäre der Zustand korrekt, käme es nicht zu der Ausnahme. Bei statischen Methoden sollte es eine IllegalStateException nicht geben.[ 183 ](Im .NET-Framework gibt es eine vergleichbare Ausnahme, die System.InvalidOperationException. In Java trifft der Name allerdings das Problem etwas besser. )
UnsupportedOperationException
Implementieren Klassen Schnittstellen oder realisieren Klassen abstrakte Methoden von Oberklassen, so muss es immer eine Implementierung geben, auch wenn die Unterklasse die Operation eigentlich gar nicht umsetzen kann oder will. Anstatt den Rumpf der Methode nur leer zu lassen und einen potenziellen Aufrufer glauben zu lassen, die Methode führe etwas aus, sollten diese Methoden eine UnsupportedOperationException auslösen. Die API-Dokumentation kennzeichnet abstrakte Methoden, die Unterklassen vielleicht nicht realisieren wollen, als optionale Operationen (siehe Abbildung 8.11).
Unglücklicherweise gibt es auch eine javax.naming.OperationNotSupportedException. Doch diese sollte nicht verwendet werden. Sie ist speziell für Namensdienste vorgesehen und auch keine RuntimeException.
IndexOutOfBoundsException
Eine IndexOutOfBoundsException löst die JVM automatisch aus, wenn zum Beispiel ein Programm die Grenzen eines Arrays missachtet. Wir können diesen Ausnahmetyp selbst immer dann nutzen, wenn wir Indexzugriffe haben, etwa auf eine Zeile in einer Datei, und wenn der Index im falschen Bereich liegt. Von IndexOutOfBoundsException gibt es die Unterklassen ArrayIndexOutOfBoundsException und StringIndexOutOfBoundsException. In der Regel nutzen Programmierer diese Typen aber nicht. Inkonsistenzen gibt es beim Einsatz von IllegalArgumentException und IndexOutOfBoundsException. Ist etwa der Index falsch, so entscheiden sich einige Autoren für den ersten Ausnahmetyp, andere für den zweiten. Beides ist prinzipiell gültig. Die IndexOutOfBoundsException ist aber konkreter und zeigt eher ein Implementierungsdetail an. Der parametrisierte Konstruktor IndexOutOfBoundsException(int) nimmt im Ganzzahl-Parameter den falschen Index an. Der Index sollte immer gemeldet werden, um die Fehlersuche zu vereinfachen.
public IndexOutOfBoundsException(int index) {
super("Index out of range: " + index);
}
Eigene NullPointerException auslösen?
Eine NullPointerException gehört mit zu den häufigsten Ausnahmen. Die JVM löst diese Ausnahme etwa bei folgendem Programmstück aus:
String s = null;
s.length(); // NullPointerException
Eine NullPointerException zeigt immer einen Programmierfehler in einem Stück Code an, und so hat es in der Regel keinen Sinn, diese Ausnahmen abzufragen – der Programmierfehler muss behoben werden. Ein Programmierer löst eine NullPointerException selten selbst aus, sondern das macht die JVM automatisch. Sie kann jedoch vom Entwickler bewusst ausgelöst werden, wenn eine zusätzliche Nachricht Klarheit verschaffen soll, oder früh beim Prüfen von Parametern am Kopf einer Methode.
Oft gibt es diese NullPointerException, wenn an Methoden null-Werte übergeben wurden. Hier muss aus der API-Dokumentation klar hervorgehen, ob null als Argument erlaubt ist oder nicht. Wenn nicht, ist es völlig in Ordnung, wenn die Methode eine NullPointerException auslöst, wenn fälschlicherweise doch null übergeben wurde. Auf null zu prüfen, um dann zum Beispiel eine IllegalArgumentException auszulösen, ist eigentlich nicht nötig. Allerdings gilt auch hier, dass eine IllegalArgumentException allgemeiner und weniger implementierungsspezifisch als eine NullPointerException ist.
[»] Hinweis
Um eine NullPointerException auszulösen, ist statt throw new NullPointerException(); auch einfach ein throw null; möglich. Doch da eine selbst aufgebaute NullPointerException ohne zusätzliche Fehlernachricht selten ist, ist dieses Idiom nicht wirklich nützlich.
Fassen wir zusammen: Eine NullPointerException wird in folgenden Fällen ausgelöst:
beim Aufruf einer Objektmethode auf einem null-Objekt
beim lesenden oder schreibenden Zugriff auf einen Objektzustand auf einem null-Objekt
beim Zugriff auf die Array-Länge über length auf einem Array, das null ist
beim Elementzugriff auf ein Array, das null ist
bei throw null
8.6.3 Parameter testen und gute Fehlermeldungen
Eine IllegalArgumentException ist eine wertvolle Ausnahme, die eine interne Ausnahme anzeigt: dass nämlich eine Methode mit falschen Argumenten aufgerufen wurde. Eine Methode sollte im Idealfall alle Parameter auf ihren korrekten Wertebereich hin prüfen und nach dem Fail-fast-Verfahren arbeiten, also so schnell wie möglich eine Ausnahme melden, anstatt Fehler zu ignorieren oder zu verschleppen. Wenn etwa das Alter einer Person bei setAge(int) nicht negativ sein kann, ist eine IllegalArgumentException eine gute Wahl. Wenn der Exception-String dann noch aussagekräftig ist, hilft das bei der Behebung des Fehlers ungemein: Der Tipp ist hier, eine aussagekräftige Meldung anzugeben.
[»] Negativbeispiel
Ist der Wertebereich beim Bilden eines Teil-Strings falsch oder ist der Index für einen Array-Zugriff zu groß, kommt es zu einer StringIndexOutOfBoundsException bzw. ArrayIndexOutOfBoundsException:
System.out.println( "Orakel-Paul".substring( 0, 20 ) ); //
liefert:
java.lang.StringIndexOutOfBoundsException: begin 0, end 20, length 11
Und
System.out.println( "Orakel-Paul".toCharArray()[20] ); //
liefert:
java.lang.ArrayIndexOutOfBoundsException: Index 20 out of bounds for length 11
Da das Testen von Parametern in eine große if-throws-Orgie ausarten kann, ist es eine gute Idee, eine Hilfsklasse mit statischen Methoden wie isNull(…), isFalse(…), isInRange(…) einzuführen, die dann eine IllegalArgumentException auslösen, wenn eben der Parameter nicht korrekt ist.[ 184 ](Die müssen wir nicht selbst schreiben, da die Open-Source-Landschaft bereits mit der Klasse org. apache.commons.lang.Validate aus den Apache Commons Lang (http://commons.apache.org/proper/commons-lang) oder mit com.google.common.base.Preconditions von Google Guava (http://github.com/google/guava) Vergleichbares bietet; in jedem Fall ist eine gute Parameterprüfung bei öffentlichen Methoden von Bibliotheken ein Muss. ) Die Java-Standardbibliothek bietet drei checkXXX(…)-Methoden in der Klasse Objects, siehe Kapitel 10, »Besondere Typen der Java SE«.
null-Prüfungen
Für null-Prüfungen bietet sich die Methode Objects.requireNonNull(reference) an, die immer dann eine NullPointerException auslöst, wenn reference gleich null ist. Optional als zweites Argument lässt sich die Fehlermeldung angeben.
[»] Tool-Unterstützung
Ein anderer Ansatz sind Prüfungen durch externe Codeprüfungsprogramme. Google zum Beispiel setzt in seinen vielen Java-Bibliotheken auf Parameter-Annotationen wie @Nonnull oder @Nullable.[ 185 ](Sie wurden in JSR-305, »Annotations for Software Defect Detection«, definiert. Java 7 sollte dies ursprünglich unterstützen, doch das wurde gestrichen. ) Statische Analysetools wie FindBugs (http://findbugs.sourceforge.net) testen dann, ob es Fälle geben kann, in denen die Methode mit null aufgerufen wird. Zur Laufzeit findet der Test jedoch nicht statt.
8.6.4 Neue Exception-Klassen deklarieren
Eigene Ausnahmen sind immer direkte (oder indirekte) Unterklassen von Exception (sie können auch Unterklassen von Throwable sein, aber das ist unüblich). Eigene Exception-Klassen bieten in der Regel zwei Konstruktoren: einen parameterlosen Konstruktor und einen mit einem String parametrisierten Konstruktor, um eine Fehlermeldung anzunehmen und zu speichern.
Um für die Klasse Player aus Listing 8.17 einen neuen Ausnahmetyp zu deklarieren, erweitern wir RuntimeException zur PlayerException:
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 IllegalArgumentException löst der Konstruktor im Fehlerfall unsere speziellere PlayerException aus:
if ( age <= 0 )
throw new PlayerException( "Kein Alter <= 0 erlaubt!" );
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" c.t.i.e.v2.PlayerException: Kein Alter <= 0 erlaubt!
at com.tutego.insel.exception.v2.Player.<init>(Player.java:9)
at com.tutego.insel.exception.v2.Player.main(Player.java:16)
[+] Tipp
Es ist immer eine gute Idee, Unterklassen von Exception zu bauen. Würden wir keine Unterklassen anlegen, sondern direkt mit throw new Exception() eine Ausnahme anzeigen, so könnten wir diese Ausnahme später nicht mehr von anderen Ausnahmen unterscheiden. Mit der Hierarchiebildung werden nämlich die Spezialisierung bei mehreren catch-Klauseln sowie eine Unterscheidung mit instanceof unterstützt. Sind die Ausnahmen exakt vom Typ Exception, müssten wir unsere Ausnahmen immer mit catch(Exception e) auffangen und bekämen so alle anderen Ausnahmen mit aufgefangen, die dann nicht mehr unterschieden werden könnten. Allerdings sollten Entwickler nicht zu inflationär mit den Ausnahmenhierarchien umgehen; in vielen Fällen reicht eine Standardausnahme aus.
8.6.5 Eigene Ausnahmen als Unterklassen von Exception oder RuntimeException?
Java steht mit der Ausnahmebehandlung über Exceptions nicht allein. Alle modernen Programmiersprachen verfügen über diese Sprachmittel. Allerdings gibt es eine Sache, die Java besonders macht: die Unterscheidung zwischen geprüften und ungeprüften Ausnahmen. Daher stellt sich beim Design von eigenen Ausnahmenklassen die Frage, ob sie eine Unterklasse von RuntimeException sein sollen oder nicht. Einige Entscheidungshilfen:
Betrachten wir, wie die Java-API geprüfte und ungeprüfte Ausnahmen einsetzt. Die ungeprüften Ausnahmen signalisieren Programmierfehler, die es zu beheben gilt. Ein gutes Beispiel ist eine NullPointerException, ClassCastException oder ArrayIndexOutOfBoundsException. Es steht außer Frage, dass Ausnahmen dieser Art Programmierfehler sind und behoben werden müssen. Ein catch wäre unnötig, da diese Ausnahme ja im korrekten Code gar nicht auftreten kann. Anders ist es bei geprüften Ausnahmen. Die Ausnahmen zeigen Fehler an, die unter gewissen Umständen einfach auftreten können. Eine IOException ist nicht schlimmer, denn die Datei kann nun einmal nicht vorhanden sein. Wir sollten uns bei dieser Unterscheidung aber bewusst sein, dass die JVM die Ausnahmen von sich aus auslöst und nicht eine Methode.
Soll sich die Anwendung von der Ausnahme »erholen« können oder nicht? Kommt es wegen einer RuntimeException zu einem Programmfehler, dann sollte die Anwendung zwar nicht »abstürzen«, allerdings ist ein sinnvolles Weiterarbeiten kaum möglich. Bei geprüften Ausnahmen ist das anders. Sie signalisieren in der Regel, dass der Fehler behoben und das Programm dann normal fortgesetzt werden kann.
Ein Modul kann intern mit RuntimeExceptions arbeiten, und der API-Designer auf der anderen Seite, der Schnittstellen zu Systemen modelliert, kann gut auf geprüfte Ausnahmen zurückgreifen. Das ist einer der Gründe, warum moderne Frameworks wie Java EE oder Spring fast ausschließlich auf RuntimeException setzen: Wenn es einen Fehler gibt, dann lässt sich schwer etwas behandeln und einfach korrigieren. Zeigt etwa ein internes Modul beim Datenbankzugriff eine Ausnahme an, muss die ganze Operation abgebrochen werden, und nichts ist zu retten. Hier gilt im Großen, was auch bei der NullPointerException im Kleinen passiert: Die Ausnahme ist ein echtes Problem, und das Programm kann nicht einfach fortgeführt werden.
Geprüfte Ausnahmen können melden, wenn sich der Aufrufer nicht an die Vereinbarung der Methode hält. Die FileNotFoundException ist so ein Beispiel.[ 186 ](Eine Reaktion wie »File not found. Should I fake it? (Y/N)« ist auch nicht so clever. ) Hätte das Programm mit der Files-Methode exists(…) vorher nach der Existenz der Datei gefragt, wäre uns diese Ausnahme erspart geblieben. (Der Sonderfall, dass die Datei beim Test noch da war, dann aber im Hintergrund gelöscht wird, ist auch nicht zu vernachlässigen.) Der Aufrufer ist sozusagen selbst schuld, dass er eine geprüfte Ausnahme bekommt, da er die Rahmenbedingungen nicht einhält. Bei einer ungeprüften Ausnahme ist nicht der Aufrufer an dem Problem schuld, sondern ein Programmierfehler. Da geprüfte Ausnahmen in der Java-Dokumentation auftauchen, ist dem Entwickler klar, was passieren wird, wenn er die Vorbedingungen der Methode nicht einhält. Nach dieser Philosophie müsste eigentlich die NumberFormatException eine geprüfte Ausnahme sein, die Integer.parseInt(…) auslöst. Denn der Entwickler hat ja die Methode parseInt(…) mit einem falschen Wert gefüttert, also den Methodenvertrag verletzt. Eine geprüfte Ausnahme wäre nach dieser Philosophie richtig. Auf der anderen Seite lässt sich argumentieren, dass das Missachten von korrekten Parametern ein interner Fehler ist, denn es ist Aufgabe des Aufrufers, das sicherzustellen, und so kann die parseInt(…) mit einer RuntimeException aussteigen.
Die Unterscheidung zwischen internen Fehlern und externen Fehlern erlaubt eine Einteilung in geprüfte und ungeprüfte Ausnahmen. Die Programmierfehler mit Ausnahmen (wie NullPointerException oder ClassCastException) lassen sich vermeiden, da wir als Programmierer unseren Quellcode kontrollieren können und die Programmfehler entfernen können. Doch bei externen Fehlern haben wir als Entwickler keine Chance. Das Netzwerk kann plötzlich zusammenbrechen und uns eine SocketException und IOException bescheren. Alles das liegt nicht in unserer Hand und kann auch durch noch so sorgsame Programmierung nicht verhindert werden. Das schwächt natürlich das Argument aus dem letzten Aufzählungspunkt ab: Es lässt sich zwar abfragen, ob eine Datei vorhanden ist, um eine FileNotFoundException abzuwehren, doch wenn die Festplatte plötzlich Feuer fängt, ist uns eine IOException gewiss, denn Java-Programme sind nicht wie folgt aufgebaut: »Frage, ob die Festplatte bereit ist, und dann lies.« Wenn der Fehler also nicht innerhalb des Programms liegt, sondern außerhalb, lassen sich geprüfte Ausnahmen verwenden.
Bei geprüften Ausnahmen in Methodensignaturen muss sich der Nutzer auf eine bestimmte API einstellen. Eine spätere Änderung des Ausnahmetyps ist problematisch, da alle catch- oder throws-Klauseln abgeändert werden müssen. RuntimeExceptions sind hier flexibler. Ändert sich bei einer agilen Programmentwicklung der Typ von geprüften Ausnahmen im Lebenslauf einer Software öfters, führt das zu vielen Änderungen, die natürlich Zeit und somit Geld kosten.
Der erste Punkt führt in der Java-API zu einigen Entscheidungen, die Entwickler quälen, aber nur konsistent sind, etwa die InterruptedException. Jedes Thread.sleep(…) zum Schlafenlegen eines Threads muss eine InterruptedException auffangen. Sie kann auftreten, wenn ein Thread von außen einen Interrupt sendet. Da das auf keinen Fall einen Fehler darstellt, ist InterruptedException eine geprüfte Ausnahme, auch wenn wir dies oft als lästig empfinden und selten auf die InterruptedException reagieren müssen. Bei einem Aufbau einer URL ist die MalformedURLException ebenfalls lästig; aber stammt die Eingabe aus einer Dialogbox, kann das Protokoll einfach falsch sein.[ 187 ](Luxuriös wäre eine Prüfung zur Compilezeit, denn wenn etwa new URL("http://de.tutego") im Code steht, so kann es die Ausnahme nicht geben. Doch von nötigen try-catch-Blöcken, je nachdem, was der Compiler statisch entscheiden kann, sind wir weit entfernt. )
Geprüfte Ausnahmen sind vielen Entwicklern lästig, was zu einem Problem führt, wenn die Ausnahmen einfach aufgefangen werden, aber nichts passiert – etwa mit einem leeren catch-Block. Die Ausnahme sollte aber vielleicht nach oben laufen. Das Problem besteht bei einer RuntimeException seltener, da sie in der Regel an der richtigen zentralen Stelle behandelt wird.
Wenn wir die Punkte genauer betrachten, dann wird schnell eine andere Tatsache klar, sodass heute eine große Unsicherheit über die richtige Exception-Basisklasse besteht. Zwar zwingt uns der Compiler, eine geprüfte Ausnahme zu behandeln, aber nichts spricht dagegen, das bei einer ungeprüften Ausnahme ebenfalls zu tun. Integer.parseInt(…) und NumberFormatException sind ein gutes Beispiel: Der Compiler zwingt uns nicht zu einem Test, wir machen ihn aber trotzdem. Sind Entwickler konsequent und prüfen sie Ausnahmen selbstständig, dann braucht der Compiler den Test prinzipiell nicht durchzuführen. Daher folgen einige Entwickler einer radikalen Strategie und entwerfen alle Ausnahmen als RuntimeException. Die Unterscheidung, ob sich eine Anwendung dann »erholen« soll oder nicht, liegt beim Betrachter und ist nur noch reine Konvention. Mit dieser Alles-ist-ungeprüft-Version würde dann Java gleichauf mit C#, C++, Python, Groovy … liegen.[ 188 ](Doch eines ist sicher: Java-Vater James Gosling ist dagegen: http://www.artima.com/intv/solid.html)
[»] Besondere Rückgaben oder Ausnahmen?
Nicht immer ist eine Ausnahme nötig, doch wann es eine Rückgabe wie null oder »–1« gibt und wann eine Ausnahme ausgelöst werden soll, ist nicht immer einfach zu beantworten und hängt vom Kontext ab. Ein Beispiel: Eine Methode liest eine Datei ein und führt eine Suche durch. Wenn eine bestimmte Teilzeichenkette nicht vorhanden ist, soll die Methode dann eine Ausnahme werfen oder nicht? Hier kommt es auf folgende Kriterien an:
Wenn das Dokument in der ersten Zeile eine Kennung tragen muss und der Test auf diese Kennung prüft, dann liegt ein Protokollfehler vor, wenn diese Kennung nicht vorhanden ist.
Im Dokument gibt es eine einfache Textsuche. Ein Suchwort kann enthalten sein, muss aber nicht.
Im ersten Fall passt eine Ausnahme gut, da ein interner Fehler vorliegt. Muss die Kennung in der Datei sein, ist sie es aber nicht, dann darf dieser Fehler nicht untergehen, und eine Ausnahme zeigt das perfekt an. Ob geprüft oder ungeprüft, steht auf einem anderen Blatt. Im zweiten Fall ist eine Ausnahme unangebracht, da es kein Fehler ist, wenn der Such-String nicht im Dokument ist; das kann vorkommen. Das ist das Gleiche wie bei indexOf(…) oder matches(…) von String – die Methoden würden ja auch keine Ausnahmen werfen, wenn es keine Übereinstimmung gibt.
8.6.6 Ausnahmen abfangen und weiterleiten *
Die Ausnahme, die ein catch-Block auffängt, kann mit einem throw wieder neu ausgelöst werden – das nennt sich rethrow. Ein Beispiel soll die Arbeitsweise verdeutlichen. Eine Hilfsmethode createUriFromHost(String) setzt vor einen Hostnamen "http://" und liefert das Ergebnis als URI-Objekt zurück. createUriFromHost("tutego.de") liefert somit einen URI mit http://tutego.de. Ist der Hostname aber falsch, löst der Konstruktor der URI-Klasse eine Ausnahme aus:
public class Rethrow {
public static URI createUriFromHost( String host ) throws URISyntaxException {
try {
return new URI( "http://" + host );
}
catch ( URISyntaxException e ) {
System.err.println( "Hilfe! " + e.getMessage() );
throw e;
}
}
public static void main( String[] args ) {
try {
createUriFromHost( "tutego.de" );
createUriFromHost( "%" );
}
catch ( URISyntaxException e ) {
e.printStackTrace();
}
}
}
Die Klasse URI testet die Strings genauer als die URL-Klasse, sodass wir in diesem Beispiel URI nutzen. Die Ausnahmen im Fehlerfall sind auch etwas anders: URISyntaxException ist die Ausnahme bei URI; MalformedURLException ist die Ausnahme bei URL. Genau diese Ausnahme provozieren wir, indem wir dem Konstruktor ein "http://%" übergeben, was ein offensichtlich falscher URI ist. Unsere Methode wird die URISyntaxException auffangen, den Fehler auf der Standardfehlerausgabe melden und dann weiterleiten, denn wirklich behandeln kann unsere Methode das Problem nicht. Sie kann nur melden, was ein Vorteil ist, wenn der Aufrufer dies nicht tut.
Die Programmausgabe ist:
Hilfe! Malformed escape pair at index 7: http://%
java.net.URISyntaxException: Malformed escape pair at index 7: http://%
at java.base/java.net.URI$Parser.fail(URI.java:2915)
…
at java.base/java.net.URI.<init>(URI.java:600)
at com.tutego.insel.exception.Rethrow.createUriFromHost(Rethrow.java:9)
at com.tutego.insel.exception.Rethrow.main(Rethrow.java:20)
8.6.7 Aufruf-Stack von Ausnahmen verändern *
Wenn wir in einer Ausnahmebehandlung eine Exception e auffangen und genau diese dann mit throw e weiterleiten, müssen wir uns bewusst sein, dass die Ausnahme e auch den Aufruf-Stack weitergibt. Greifen wir noch einmal auf das vorangehende Beispiel zurück:
Hilfe! Malformed escape pair at index 7: http://%
java.net.URISyntaxException: Malformed escape pair at index 7: http://%
at java.base/java.net.URI$Parser.fail(URI.java:2915)
…
at java.base/java.net.URI.<init>(URI.java:600)
at com.tutego.insel.exception.Rethrow.createUriFromHost(Rethrow.java:9)
at com.tutego.insel.exception.Rethrow.main(Rethrow.java:20)
Die main(…)-Methode fängt die Ausnahme von createUriFromHost(…) ab, aber diese Methode steht nicht ganz oben im Aufruf-Stack. Die Ausnahme stammte ja gar nicht von createUriFromHost(…) selbst, sondern von fail(…), sodass fail(…) oben steht. Ist das nicht gewünscht, kann es korrigiert werden, denn die Basisklasse für alle Ausnahmen Throwable bietet die Methode fillInStackTrace(), mit der sich der Aufruf-Stack neu füllen lässt. Unsere bekannte Methode createUriFromHost(…) soll auf fillInStackTrace() zurückgreifen:
public static URI createUriFromHost( String host ) throws URISyntaxException {
try {
return new URI( "http://" + host );
}
catch ( URISyntaxException e ) {
System.err.println( "Hilfe! " + e.getMessage() );
e.fillInStackTrace();
throw e;
}
}
Kommt es in createUriFromHost(…) zur URISyntaxException, so fängt unsere Methode diese ab. Ursprünglich ist in e der Aufruf-Stack mit der fail(…)-Methode ganz oben gespeichert, allerdings löscht fillInStackTrace() zunächst den ganzen Stack-Trace und füllt ihn neu mit dem Pfad, den der aktuelle Thread zu der Methode führt, die fillInStackTrace() aufruft – das ist createUriFromHost(…). Daher beginnt die Konsolenausgabe auch mit unserer Methode:
Hilfe! Malformed escape pair at index 7: http://%
java.net.URISyntaxException: Malformed escape pair at index 7: http://%
at com.….RethrowFillInStackTrace.createUriFromHost(RethrowFillInStackTrace.java:12)
at com.….RethrowFillInStackTrace.main(RethrowFillInStackTrace.java:20)
8.6.8 Präzises rethrow *
Die Notwendigkeit, Ausnahmen über einen Basistyp zu fangen, ist mit dem Einzug von multi-catch gesunken. Doch für gewisse Programmteile ist es immer noch praktisch, alle Ausnahmen eines gewissen Typs aufzufangen. Wir können auch so weit in der Ausnahmehierarchie nach oben laufen, dass wir alle Ausnahmen auffangen können – dann haben wir es mit einem try { … } catch( Throwable t){ … } zu tun. Ein multi-catch ist für geprüfte Ausnahmen besonders gut, aber bei ungeprüften Ausnahmen ist nicht immer klar, was als Ausnahme denn so ausgelöst wird, und ein catch(Throwable t) hat den Vorteil, dass es alles wegfischt.
Problemstellung
Werden Ausnahmen über einen Basistyp gefangen und wird diese Ausnahme mit throw weitergeleitet, dann ist es naheliegend, dass der aufgefangene Typ genau der Typ ist, der auch bei throws in der Methodensignatur stehen muss.
Stellen wir uns vor, ein Programmblock nimmt einen Screenshot und speichert ihn in einer Datei. Kommt es beim Abspeichern zu einer Ausnahme, soll das, was vielleicht schon in die Datei geschrieben wurde, gelöscht werden. Die Regel ist also: Entweder steht der Screenshot komplett in der Datei oder es gibt gar keine Datei. Die Methode kann wie folgt aussehen, wobei sie Ausnahmen an den Aufrufer weitergibt:
public static void saveScreenshot( String filename )
throws AWTException, IOException {
try {
Rectangle r = new Rectangle( Toolkit.getDefaultToolkit().getScreenSize() );
BufferedImage screenshot = new Robot().createScreenCapture( r );
ImageIO.write( screenshot, "png", new File( filename ) );
}
catch ( AWTException e ) {
throw e;
}
catch ( IOException e ) {
Files.delete( Paths.get( filename ) );
throw e;
}
}
Mit den beiden catch-Blöcken sind wir genau auf die Ausnahmen eingegangen, die createScreenCapture(Rectangle) und write(…) auslösen. Das ist richtig, aber löschen wir wirklich immer die Dateireste, wenn es Probleme beim Schreiben gibt? Richtig ist, dass wir immer dann die Datei löschen, wenn es zu einer IOException kommt. Aber was passiert, wenn die Implementierung eine RuntimeException auslöst? Dann wird die Datei nicht gelöscht, aber das ist gefragt! Das scheint einfach gefixt, denn statt
catch ( IOException e ) {
// Datei löschen
throw e;
}
schreiben wir:
catch ( Throwable e ) {
// Datei löschen
throw e;
}
Doch können wir das Problem so lösen? Der Typ Throwable passt doch gar nicht mehr mit dem deklarierten Typ IOException in der Methodensignatur zusammen:
public static void saveScreenshot( String filename )
throws AWTException /*1*/, IOException /*2*/ {
...
catch ( AWTException /*1*/ e ) {
throw e;
}
catch ( Throwable /*?*/ e ) {
Files.delete( Paths.get( filename ) );
throw e;
}
}
Die erste catch-Klausel fängt die AWTException und leitet sie weiter. Damit wird saveScreenshot(String) zum möglichen Auslöser von AWTException, und die Ausnahme muss mit throws an die Signatur gesetzt werden. Wenn nun ein catch-Block jedes Throwable auffängt und dieses Throwable weiterleitet, ist zu erwarten, dass an der Signatur auch Throwable stehen muss und IOException nicht reicht. Das war auch bis Java 6 so, aber in Java 7 erfolgte eine Anpassung von einer unpräziseren hin zu einer präziseren Typanalyse.
Präzisere Typprüfung
Der Java-Compiler führt bei Ausnahmen eine präzise Typanalyse durch: Immer dann, wenn in einem catch-Block ein throw stattfindet, ermittelt der Compiler die im try-Block tatsächlich aufgetretenen geprüften Exception-Typen und schenkt dem im catch genannten Typ für das rethrow im Prinzip keine Beachtung. Statt des gefangenen Typs wird der Compiler den durch die Codeanalyse gefundenen Typ beim rethrow melden.
Der Compiler erlaubt nur dann das präzise rethrow, wenn die catch-Variable nicht verändert wird. Zwar ist eine Veränderung einer nichtfinalen catch-Variablen wie auch unter Java 1.0 erlaubt, doch wenn die Variable belegt wird, schaltet der Compiler von der präzisen in die unpräzise Erkennung zurück. Führen wir etwa die folgende Zuweisung ein, so funktioniert das Ganze schon nicht mehr:
catch ( Throwable e ) {
// Datei löschen
e = new IllegalStateException();
throw e;
}
Die Zuweisung führt zu dem Compilerhinweis, dass jetzt auch Throwable mit in die throws-Klausel muss.
[»] Stilfrage
Die catch-Variable kann für die präzisere Typprüfung den Modifizierer final tragen, muss das aber nicht tun. Immer dann, wenn es keine Veränderung an der Variablen gibt, wird der Compiler sie als final betrachten und eine präzisere Typprüfung durchführen – daher nennt sich das auch effektiv final. Die Java Language Specification rät vom final-Modifizierer aus Stilgründen ab. Es ist daher Quatsch, überall ein final dazuzuschreiben, um die präzisere Typprüfung zu dokumentieren.
[»] Migrationsdetail
Da der Compiler nun mehr Typwissen hat, stellt sich die Frage, ob alter Programmode mit dem neuen präziseren Verhalten vielleicht ungültig werden könnte. Theoretisch ist das möglich, aber die Sprachdesigner haben in über 9 Millionen Zeilen Code[ 189 ](Die Zahl stammt aus der FOSDEM-Präsentation 2011 »Project Coin: Language Evolution in the Open«. ) von unterschiedlichen Projekten keine Probleme gefunden. Prinzipiell könnte der Compiler jetzt unerreichbaren Code finden, der vorher versteckt blieb. Hier folgt ein kleines Beispiel, das vor Java 7 compiliert, aber ab Java 7 nicht mehr:
try {
throw new FileNotFoundException();
}
catch ( IOException e ) {
try {
throw e; // e ist für den Compiler vom Typ FileNotFoundException
}
catch ( MalformedURLException f ) { }
}
Die Variable e in catch (IOException e) ist effektiv final, und der Compiler führt die präzisere Typerkennung durch. Er findet heraus, dass der wahre rethrow-Typ nicht IOException, sondern FileNotFoundException ist. Wenn dieser Typ dann mit throw e weitergeleitet wird, kann ihn catch(MalformedURLException) nicht auffangen. Vor Java 7 war das etwas anders, denn hier wusste der Compiler nur, dass e irgendeine IOException ist, und es hätte ja durchaus die IOException-Unterklasse MalformedURLException sein können. (Warum MalformedURLException aber eine Unterklasse von IOException ist, steht auf einem ganz anderen Blatt.)
8.6.9 Geschachtelte Ausnahmen *
Der Grund für eine Ausnahme mag der 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 Ausnahmen ist das nicht anders. Nehmen wir an, wir haben eine Methode foo(), die im Falle eines Misslingens eine Ausnahme HellException auslöst. Ruft unsere Methode foo() nun ein Unterprogramm bar() auf, das zum Beispiel eine Ein-/Ausgabeoperation tätigt, und geht das schief, wird die IOException der Anlass für unsere HellException sein. Es liegt also nahe, bei der Nennung des Grunds für das eigene Versagen das Misslingen der Unteraufgabe zu nennen (wieder ein Beweis dafür, 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:
class java.lang.Throwable
implements Serializable
Throwable(Throwable cause)
Throwable(String message, Throwable cause)
Den Grund der Ausnahme erfragt die Methode Throwable getCause().
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 Konstruktortypen mit Throwable-Parameter, wie IOException, SQLException oder ClassNotFoundException, andere wiederum nicht, wie PrinterException. Eigene Unterklassen können auch mit initCause (Throwable) genau einmal eine geschachtelte Ausnahme angeben.
Geprüfte Ausnahmen in ungeprüfte Ausnahmen verpacken
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.
Dazu ein Beispiel. Die folgenden drei Zeilen ermitteln, ob die Webseite zu einer URL verfügbar ist:
HttpURLConnection.setFollowRedirects( false );
HttpURLConnection con = (HttpURLConnection)(new URL( url ).openConnection());
boolean available = con.getResponseCode() == HttpURLConnection.HTTP_OK;
Da der Konstruktor von URL eine MalformedURLException auslösen kann und es beim Netzwerkzugriff zu einer IOException kommen kann, müssen diese beiden Ausnahmen entweder behandelt oder an den Aufrufer weitergereicht werden. (MalformedURLException ist eine spezielle IOException, das verkürzt das Programm etwas.) Wir wollen eine Variante wählen, in der wir die geprüften Ausnahmen in eine RuntimeException hüllen, sodass es eine Utility-Methode gibt und sich der Aufrufer nicht lange mit irgendwelchen Ausnahmen beschäftigen muss:
public static boolean isAvailable( String url ) {
try {
HttpURLConnection.setFollowRedirects( false );
HttpURLConnection con = (HttpURLConnection)(new URL( url ).openConnection());
return con.getResponseCode() == HttpURLConnection.HTTP_OK;
}
catch ( IOException e ) {
throw new RuntimeException( e );
}
}
public static void main( String[] args ) {
System.out.println( isAvailable( "http://laber.rhabar.ber/" ) ); // false
System.out.println( isAvailable( "http://www.tutego.de/" ) ); // true
System.out.println( isAvailable( "taube://sonsbeck/schlossstrasse/5/" ) ); //
}
In der letzten Zeile kommt es zu einer Ausnahme, da es das Protokoll »taube« nicht gibt. Doch das Programm bricht schon vorher bei der URL "http://laber.rhabar.ber" ab, da keine Verbindung zum Server möglich ist. Die Ausgabe ist folgende:
Exception in thread "main" java.lang.RuntimeException: java.net.UnknownHostException: laber.rabar.ber
at com.tutego.insel.exception.NestedException.isAvailable(NestedException.java:15)
at com.tutego.insel.exception.NestedException.main(NestedException.java:23)
Caused by: java.net.UnknownHostException: laber.rabar.ber
at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:220)
...
at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:527)
at com.tutego.insel.exception.NestedException.isAvailable(NestedException.java:12)
... 1 more
In der Praxis ist es bei großen Stack-Traces – und einem Szenario, bei dem abgefangen und neu verpackt wird – fast unmöglich, aus der Ausgabe den Verlauf zu entschlüsseln, da sich diverse Teile wiederholen und dann wieder abgekürzt werden. Die duplizierten Teile sind zur Verdeutlichung fett hervorgehoben.
[»] Hinweis
Anstatt den parametrisierten Konstruktor new RuntimeException(e) zu verwenden, hätten wir auch initCause(…) verwenden können:
catch ( IOException e ) {
RuntimeException e2 = new RuntimeException();
e2.initCause( e );
throw e2;
}
Allerdings gibt es einen wichtigen Unterschied: Ohne den parametrisierten Konstruktor und mit initCause(…) gibt es keinen automatischen Aufruf von fillInStackTrace(), daher sieht der Stack-Trace unterschiedlich aus.
UncheckedIOException
Ausnahmen bei Ein-/Ausgabe-Operationen werden in Java traditionell über eine geprüfte Ausnahme vom Typ IOException gemeldet. Bei Frameworks ist das zum Teil etwas lästig, sodass es im Paket java.io eine Art Wrapper-Klasse gibt, die eine geprüfte IOException in einer ungeprüften UncheckedIOException einbettet.
class java.io.UncheckedIOException
extends RuntimeException
UncheckedIOException(IOException cause)
Ummantelt cause.UncheckedIOException(String message, IOException cause)
Ummantelt cause mit einer zusätzlichen Meldung.
Bisher macht die Java-Bibliothek nur an einer Stelle von diesem Ausnahmetyp Gebrauch, und das ist bei lines() der Klasse BufferedReader, damit bei der Stream-API die geprüften Ausnahmen nicht im Weg stehen.