9.3 Die Klassenhierarchie der Ausnahmen
Eine Ausnahme ist ein Objekt, dessen Typ direkt oder indirekt von java.lang.Throwable abgeleitet ist. (Die Namensgebung mit -able legt eine Schnittstelle nahe, aber Throwable ist eine nichtabstrakte Klasse.) Wir haben selbst wenig mit Throwable zu tun, sondern eher mit der direkten Unterklasse Exception bzw. den davon abgeleiteten Ausnahmeklassen.
9.3.1 Eigenschaften des Exception-Objekts
Das Ausnahmeobjekt, das uns in der catch-Klausel übergeben wird, ist reich an Informationen. So lässt sich erfragen, um welche Ausnahme es sich eigentlich handelt und wie die Fehlernachricht heißt. Auch der Stack-Trace lässt sich erfragen und ausgeben:
try {
Integer.parseInt( "19%" );
}
catch ( NumberFormatException e ) {
String name = e.getClass().getName();
String msg = e.getMessage();
String s = e.toString();
System.out.println( name );// java.lang.NumberFormatException
System.out.println( msg ); // For input string: "19%"
System.out.println( s ); // java.lang.NumberFormatException: For input string: "19%"
e.printStackTrace();
}
Im letzten Fall, mit e.printStackTrace(), bekommen wir auf dem Fehlerkanal System.err das Gleiche ausgegeben, was uns die virtuelle Maschine ausgibt, wenn wir die Ausnahme nicht abfangen:
java.lang.NumberFormatException
For input string: "19%"
java.lang.NumberFormatException: For input string: "19%"
java.lang.NumberFormatException: For input string: "19%"
at java.base/java.lang.NumberFormatException.forInputString(
NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at c.t.i.e.NumberFormatExceptionElements.main(NumberFormatExceptionElements.java:7)
Die Ausgabe besteht aus dem Klassennamen der Exception, der Meldung und dem Stack-Trace. printStackTrace(…) ist parametrisiert und kann auch in einen Ausgabekanal geschickt werden.
9.3.2 Basistyp Throwable
Alle Ausnahmen sind Unterklassen von Throwable. Von dort aus verzweigt sich die Hierarchie der Ausnahmearten nach java.lang.Exception und java.lang.Error. Die Klassen, die aus Error hervorgehen, sollen hier nicht weiterverfolgt werden. Es handelt sich bei ihnen um so schwerwiegende Ausnahmen, dass sie zur Beendigung des Programms führen und vom Programmierer nicht weiter beachtet werden müssen und sollten. Throwable vererbt eine Reihe von nützlichen Methoden, die in Abbildung 9.4 zu sehen sind. Sie fasst gleichzeitig die Vererbungsbeziehungen noch einmal zusammen.
9.3.3 Die Exception-Hierarchie
In der Bibliothek sind alle Ausnahmeklassen von java.lang.Exception abgeleitet. Die Exceptions sind Fehler oder Ausnahmesituationen, die uns von behandelt werden sollten. Die Klasse Exception teilt sich dann nochmals in weitere Unterklassen bzw. Unterhierarchien auf. Abbildung 9.5 zeigt einige Unterklassen der Klasse Exception.
9.3.4 Oberausnahmen auffangen
Eine Konsequenz der Hierarchien besteht darin, dass es ausreicht, eine Ausnahme der Oberklasse aufzufangen. Wenn zum Beispiel eine FileNotFoundException auftritt, ist diese Klasse von IOException abgeleitet, was bedeutet, dass FileNotFoundException eine Spezialisierung darstellt. Wenn wir jede IOException auffangen, behandeln wir damit auch gleichzeitig die FileNotFoundException mit (siehe Abbildung 9.6). Nehmen wir folgende Behandlung an:
try {
...
}
catch ( FileNotFoundException e ) {
e.printStackTrace();
}
catch ( IOException e ) {
e.printStackTrace();
}
Da die Behandlung in den catch-Blöcken identisch ist, kann catch (IOException e) die FileNotFoundException gleich mitfangen:
Auch wenn die Ausnahme über eine Oberklasse aufgefangen wird, lässt sich die Ausnahme prinzipiell später wieder mit instanceof identifizieren. Wir könnten schreiben:
catch ( IOException e ) {
if ( e instanceof FileNotFoundException )
System.err.println( "Datei ist nicht vorhanden!" );
else
System.err.println( "Allgemeiner Ein-/Ausgabefehler!" );
}
Aus Gründen der Übersichtlichkeit sollte diese Technik jedoch sparsam angewendet werden.
9.3.5 Schon gefangen?
Der Java-Compiler prüft, ob Ausnahmen vielleicht schon in der Kette aufgefangen wurden, und meldet einen Fehler, wenn catch-Blöcke nicht erreichbar sind. Wir haben gesehen, dass FileNotFoundException eine spezielle IOException ist und ein catch(IOException e) Ausnahmen vom Typ FileNotFoundException gleich mit fängt.
try {
...
}
catch ( IOException e ) { // fange IOException und alle Unterklassen auf
}
Natürlich kann eine FileNotFoundException weiterhin als eigener Typ aufgefangen werden, allerdings ist es wichtig, die Reihenfolge der catch-Blöcke zu beachten. Denn die Reihenfolge ist absolut relevant; die Typtests beginnen oben und laufen dann weiter nach unten durch. Wenn ein früher catch schon Ausnahmen eines gewissen Typs abfängt, also etwa ein catch auf IOException alle Ein-/Ausgabefehler, so ist ein nachfolgender catch auf die FileNotFoundException falsch.
Nehmen wir an, ein try-Block kann eine FileNotFoundException und eine IOException auslösen. Dann ist die linke Behandlung aus Tabelle 9.1 korrekt, aber die rechte falsch:
richtig |
mit Compilerfehler |
---|---|
try { |
try { |
[»] Hinweis
Der folgende multi-catch ist falsch:
try {
new RandomAccessFile( "", "" );
}
catch ( FileNotFoundException | IOException | Exception e ) { }
Der Java-Compiler meldet einen Fehler der Art »Alternatives in a multi-catch statement cannot be related by subclassing« und bricht ab.
Mengenprüfungen führt der Compiler auch ohne multi-catch durch, und Folgendes ist, wie gerade besprochen, ebenfalls falsch:
try { new RandomAccessFile( "", "" ); }
catch ( Exception e ) { }
catch ( IOException e ) { }
catch ( FileNotFoundException e ) { }
Während eine Umsortierung der Zeilen die Fehler korrigiert, spielt die Reihenfolge bei multi-catch keine Rolle.
9.3.6 Ablauf einer Ausnahmesituation
Das Laufzeitsystem erzeugt ein Ausnahmeobjekt, wenn ein Fehler über eine Exception angezeigt werden soll. Dann wird die Abarbeitung der Programmzeilen sofort unterbrochen, und das Laufzeitsystem steuert selbstständig die erste catch-Klausel an (oder springt weiter zum Aufrufer bei einem throws oder einer ungeprüften Ausnahme). Wenn die erste catch-Klausel nicht zur Art der aufgetretenen Ausnahme passt, werden der Reihe nach alle übrigen catch-Klauseln untersucht, und die erste übereinstimmende Klausel wird angesprungen (oder ausgewählt). Erst wird etwas versucht (daher heißt es im Englischen try), und wenn im Fehlerfall ein Exception-Objekt im Programmstück ausgelöst und geworfen (engl. throw) wird, lässt es sich an einer Stelle auffangen (engl. catch). Da immer die erste passende catch-Klausel ausgewählt wird, darf im Beispiel die letzte catch-Klausel keinesfalls zuerst stehen, da diese auf jede IOException passt, und FileNotFoundException ist eine Unterklasse von IOException. Alle anderen Anweisungen in den catch-Blöcken würden dann nicht ausgeführt; der Compiler erkennt dieses Problem und gibt einen Fehler aus.
9.3.7 Nicht zu allgemein fangen!
Löst ein Programmblock etwa eine IOException, MalformedURLException und eine FileNotFoundException aus und sollen die drei Ausnahmen gleich behandelt werden, so fängt ein catch (IOException e) die beiden Typen FileNotFoundException und MalformedURLException gleich mit ab, da beide Unterklassen von IOException sind. So behandelt ein Block alle drei Ausnahmetypen. Das ist praktisch.
Nun gibt es jedoch auch Ausnahmen, die in der Vererbungsbeziehung nebeneinanderliegen, wie InputMismatchException und IOException. Erinnern wir uns noch einmal an das Dateibeispiel der Klasse UserInputUuidWriterMultiCatch in Abbildung 9.6, wo wir ein multi-catch eingesetzt haben:
Da InputMismatchException und IOException Unterklassen von Exception sind, lässt sich im Prinzip auch schreiben:
try {
...
}
catch ( Exception e ) {
...
}
Die hier gewählte Lösung, in der Ausnahmehierarchie so weit nach oben zu laufen, bis eine gemeinsame Oberklasse gefunden wurde, ist nicht gut. Denn was für Ausnahmetypen gut funktioniert, die sowie in der Hierarchie liegen (wie MalformedURLException, was eine IOException ist), ist catch(Exception e) gefährlich, weil damit viel mehr aufgefangen und in der Ausnahmebehandlung behandelt wird. Taucht beispielsweise eine null-Referenz durch eine nicht initialisierte Variable mit Referenztyp auf und kommt es zu einer NullPointerException, so würde dies fälschlicherweise ebenso mitgefangen – der Programmfehler hat aber nichts mit der InputMismatchException oder IOException zu tun:
try {
Point p = null;
p.x = 2; // NullPointerException
int i = 0;
int x = 12 / i; // Ganzzahlige Division durch 0
irgendwas kann InputMismatchException auslösen ...
irgendwas kann IOException auslösen ...
}
catch ( Exception e ) { Behandlung }
Eine NullPointerException und die ArithmeticException sollen nicht mitbehandelt werden. Das zentrale Problem ist hier, dass diese Ausnahmen ungeprüfte Ausnahmen vom Typ RuntimeException sind und RuntimeException eine Unterklasse von Exception ist. Fangen wir alle Exception-Typen, so wird alles mitgefangen – und RuntimeException eben auch. Es ist nicht möglich, alle Nicht-Laufzeitausnahmen abzufangen, was etwa funktionieren würde, wenn RuntimeException keine Unterklasse von Exception wäre, etwa ein Throwable – aber das haben die Sprachdesigner nicht so modelliert.
Wenn main(String[]) alles weiterleitet
Ist die Ausnahmebehandlung in einem Hauptprogramm ganz egal, so können wir alle Ausnahmen auch an die Laufzeitumgebung weiterleiten, die dann das Programm – genau genommen den Thread – im Fehlerfall abbricht:
public static void main( String[] args ) throws Exception {
Scanner in = new Scanner( Paths.get( "lyrics.txt" ) );
System.out.println( in.nextLine() );
}
Das funktioniert, da alle Ausnahmen von der Klasse Exception[ 184 ](Genauer gesagt, sind alle Ausnahmen in Java von der Exception-Oberklasse Throwable abgeleitet. ) abgeleitet sind. Wird die Ausnahme nirgendwo sonst aufgefangen, erfolgt die Ausgabe einer Laufzeitfehlermeldung, denn das Exception-Objekt ist beim Interpreter, also bei der virtuellen Maschine, auf der äußersten Aufrufebene gelandet. Natürlich ist das kein guter Stil (obwohl es auch in diesem Buch so gemacht wird, um Programme kurz zu halten), denn Ausnahmen sollten in jedem Fall behandelt werden.
9.3.8 Bekannte RuntimeException-Klassen
Die Java-API bietet insgesamt eine große Anzahl von RuntimeException-Klassen, und es werden immer mehr. Tabelle 9.2 listet einige bekannte Ausnahmetypen auf und zeigt, welche Operationen die Ausnahmen auslösen. Wir greifen hier schon auf spezielle APIs vor, die erst später im Buch vorgestellt werden.
Unterklasse von RuntimeException |
was den Fehler auslöst |
---|---|
ganzzahlige Division durch 0 |
|
Indexgrenzen wurden missachtet, etwa durch (new int[0])[1]. Eine ArrayIndexOutOfBoundsException ist neben der StringIndexOutOfBoundsException eine Unterklasse von IndexOutOfBoundsException. |
|
Eine Typumwandlung ist zur Laufzeit nicht möglich. So löst String s = (String) new Object(); eine ClassCastException mit der Meldung »java.lang. Object cannot be cast to java.lang.String« aus. |
|
Der Stapelspeicher ist leer. new java.util.Stack(). pop() provoziert den Fehler. |
|
Eine häufig verwendete Ausnahme, mit der Methoden falsche Argumente melden. Integer.parseInt("tutego") löst eine NumberFormatException, eine Unterklasse von IllegalArgumentException, aus. |
|
Ein Thread möchte warten, hat aber den Monitor nicht. Ein Beispiel: new String().wait(); |
|
Meldet einen der häufigsten Programmierfehler, beispielsweise durch ((String) null).length(). |
|
Operationen sind nicht gestattet, etwa durch java.util.Arrays.asList(args).add("jv"). |
9.3.9 Kann man abfangen, muss man aber nicht
Eine RuntimeException kann im Code abgefangen werden, muss es aber nicht. Da der Compiler nicht auf einem Abfangen besteht, heißen die aus RuntimeException hervorgegangenen Ausnahmen auch ungeprüfte Ausnahmen bzw. nichtgeprüfte Ausnahmen (engl. unchecked exceptions), und alle übrigen heißen geprüfte Ausnahmen (engl. checked exceptions). Auch muss eine RuntimeException nicht unbedingt bei throws in der Methodensignatur angegeben werden, obwohl einige Autoren das zur Dokumentation machen.
Praktisch ist, dass eine ungeprüfte Ausnahme entlang der Kette von Methodenaufrufen wie eine Blase (engl. bubble) nach oben steigen und irgendwann von einem Block abgefangen werden kann, der sich darum kümmert. Tritt eine RuntimeException zur Laufzeit auf und kommt nicht irgendwann in der Aufrufhierarchie ein try-catch, beendet die JVM den ausführenden Thread. Löst also eine in main(…) aufgerufene Aktion eine RuntimeException aus, ist das das Ende für dieses Hauptprogramm.
[+] Style
Eine RuntimeException wird nicht im Methodenkopf angegeben, sollte aber im Javadoc dokumentiert werden.