8.2 Die Klassenhierarchie der Fehler 

Eine Exception 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 nicht-abstrakte Klasse.) Von dort aus verzweigt sich die Hierarchie der Fehlerarten nach java.lang.Exception und java.lang.Error. Die Klassen, die aus Error hervorgehen, sollen nicht weiterverfolgt werden. Es handelt sich hierbei um so schwerwiegende Fehler, dass sie zur Beendigung des Programms führen und vom Programmierer nicht weiter beachtet werden müssen und sollen. Throwable vererbt eine Reihe von nützlichen Methoden, die in der folgenden Grafik sichtbar sind. Sie fasst gleichzeitig die Vererbungsbeziehungen noch einmal zusammen.
8.2.1 Die Exception-Hierarchie 

Jede Benutzerausnahme wird von java.lang.Exception abgeleitet. Die Exceptions sind Fehler oder Ausnahmesituationen, die vom Programmierer behandelt werden sollen. Die Klasse Exception teilt sich dann nochmals in weitere Unterklassen beziehungsweise Unterhierarchien auf. Die folgende Grafik zeigt einige Unterklassen der Klasse Exception:
8.2.2 Oberausnahmen auffangen 

Eine Konsequenz der Hierarchien besteht darin, dass es ausreicht, einen Fehler 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.
Erinnern wir uns noch einmal an das Dateibeispiel. Dort haben wir eine FileNotFoundException und eine IOException einzeln behandelt. Dies lässt sich wie folgt zusammenfassen:
Listing 8.9 ReadFileWithRAFShort.java, main()
RandomAccessFile f = null; try { f = new RandomAccessFile( "c:/winnt/desktop.ini", "r" ); for ( String line; (line=f.readLine()) != null; ) System.out.println( line ); } catch ( IOException e ) { System.err.println( "Ein-/Ausgabe-Probleme." ); } finally { if ( f != null ) try { f.close(); } catch ( IOException e ) { e.printStackTrace(); } }
Angst davor, dass wir den Fehlertyp später nicht mehr unterscheiden können, brauchen wir nicht zu haben, denn die an die catch-Anweisung gebundenen Variablen können wir mit instanceof weiter verfeinern. Aus Gründen der Übersichtlichkeit sollte diese Technik jedoch sparsam angewendet werden. Fehlerarten, die unterschiedlich behandelt werden müssen, verdienen immer getrennte catch-Klauseln. Das trifft zum Beispiel auf FileNotFoundException und IOException zu.
8.2.3 Alles geht als Exception durch 

Da Exception die Basisklasse aller Exceptions ist, ließe sich natürlich auch alles mit Exception abfangen. So könnte jemand auf die Idee kommen, aus
try { irgendwas Unartiges ... irgendwas anderes Unartiges ... } catch ( IllegalAccessException e ) { Behandlung } catch ( InstantiationException e ) { Behandlung }
aufgrund der identischen Fehlerbehandlungen eine Optimierung zu versuchen, die etwa so aussieht:
try { irgendwas Unartiges ... irgendwas anderes Unartiges ... } catch ( Exception e ) { Behandlung }
Da der Aufruf in den catch-Blöcken gleich aussieht, ließe sich alles in einer Routine zur Fehlerbehandlung ausführen. Doch dann muss die Oberklasse genommen werden – sozusagen der kleinste gemeinsame Nenner –, und dies ist die Klasse Exception. Doch was für andere Fehlertypen gut funktionieren mag, ist für catch(Exception) gefährlich, weil wirklich jede Ausnahme aufgefangen und in der Ausnahmebehandlung bearbeitet wird. Taucht beispielsweise eine null-Referenz durch eine nicht initialisierte Variable mit Referenztyp auf, so würde dies fälschlicherweise ebenso behandelt.
Point p = null; p.setX( 2 ); // Nicht initialisiert int i = 0; int x = 12 / i; // Ganzzahlige Division durch 0
Tritt eine Exception auf, so wird sie im Ausgabefenster rot angezeigt. Praktischerweise sind die Fehlermeldungen wie links: Ein Klick, und Eclipse zeigt die Zeile, die die Exception auslöst.
8.2.4 RuntimeException muss nicht aufgefangen werden 

Einige Fehlerarten können potentiell an vielen Programmstellen auftreten, etwa eine ganzzahlige Division durch null [Fließkommadivisionen durch 0.0 ergeben entweder ± unendlich oder NaN.] oder ungültige Indexwerte beim Zugriff auf Array-Elemente. Treten solche Fehler beim Programmlauf auf, liegt in der Regel ein Denkfehler des Programmierers vor, und das Programm sollte normalerweise nicht versuchen, die ausgelöste Ausnahme aufzufangen und zu behandeln. Daher wurde die Unterklasse RuntimeException eingeführt, die Fehler beschreibt, die vom Programmierer behandelt werden können, aber nicht müssen. Der Name »RuntimeException« ist jedoch seltsam gewählt, da alle Ausnahmen immer zur Runtime, also zur Laufzeit, erzeugt, ausgelöst und behandelt werden. Tabelle 8.1 listet einige bekannte Fehlertypen auf:
Unterklasse von RuntimeException | Was den Fehler auslöst |
ArithmeticException |
ganzzahlige Division durch 0 |
ArrayIndexOutOfBoundsException |
Indexgrenzen missachtet, etwa durch (new int[0])[1]. Eine ArrayIndexOutOfBoundsException ist neben StringIndexOutOfBoundsException eine Unterklasse von IndexOutOfBoundsException. |
ClassCastException |
Typanpassung ist zur Laufzeit nicht möglich. So löst (java.util.Stack) new java.util.Vector() eine ClassCastException mit der Meldung »java.util.Vector cannot be cast to java.util.Stack« aus. |
EmptyStackException |
Stapelspeicher ist leer. Die Anweisung new java.util.Stack().pop(); provoziert den Fehler. |
IllegalArgumentException |
Eine häufig verwendete Ausnahme, mit der Methoden falsche Argumente melden. Die Anweisung Integer.parseInt("tutego"); löst eine NumberFormatException, eine Unterklasse von IllegalArgumentException, aus. |
IllegalMonitorStateException |
Thread möchte warten, hat aber den Monitor nicht. Ein Beispiel: new String().wait(); |
NullPointerException |
Meldet einen der häufigsten Programmierfehler, beispielsweise durch ((String) null).length(). |
UnsupportedOperationException |
Operationen sind nicht gestattet, etwa durch java.util.Arrays.asList(args).add("chris");. |
Eine RuntimeException muss der Entwickler nicht abfangen, kann er aber. Da der Compiler nicht auf ein Abfangen besteht, heißen die aus RuntimeException hervorgegangen Ausnahmen auch nicht geprüfte Ausnahmen (engl. unchecked exceptions) und alle übrigen geprüfte Ausnahmen (engl. checked exceptions). Auch muss eine RuntimeException nicht unbedingt bei throws in der Methodensignatur angegeben werden, wobei einige Autoren das zur Dokumentation machen. Tritt eine RuntimeException zur Laufzeit auf und kommt nicht irgendwann in der Aufrufhierarchie ein try-catch, wird der ausführende Thread beendet. Löst also eine in main() aufgerufene Aktion eine RuntimeException aus, ist das das Ende für dieses Hauptprogramm.
8.2.5 Harte Fehler: Error 

Fehler, die von der Klasse java.lang.Error abgeleitet sind, stellen Fehler dar, die mit der JVM in Verbindung stehen. Anders dagegen die von Exception abgeleiteten Klassen – sie stehen für eigene Programmfehler. Beispiele für konkrete Error-Klassen sind AnnotationFormatError, AssertionError, AWTError, CoderMalfunctionError, FactoryConfigurationError, LinkageError, ThreadDeath, TransformerFactoryConfigurationError, VirtualMachineError (mit den Unterklassen InternalError, OutOfMemoryError, StackOverflowError, UnknownError). Im Fall von ThreadDeath lässt sich ableiten, dass nicht alle Error-Klassen auf »Error« enden. Das liegt sicherlich auch daran, dass das nicht ein Fehler im eigentlichen Sinne ist, denn die JVM löst ThreadDeath aus, wenn das Programm einen Thread mit stop() beenden will.
Da die Fehler »abnormales« Verhalten anzeigen, müssen sie auch nicht mit einem try-catch-Block aufgefangen oder mit throws nach oben weitergegeben werden. (Sun zählt sie daher auch zu den nicht geprüften Ausnahmen, obwohl Error keine Unterklasse von RuntimeException ist!) Allerdings ist es möglich, die Fehler aufzufangen, da Error-Klassen Unterklassen von Throwable sind und sich daher genauso behandeln lassen. Insofern ist ein Auffangen legitim, und auch ein finally ist korrekt. Ob das Auffangen sinnvoll ist, ist eine andere Frage, denn wenn die JVM einen Fehler anzeigt, bleibt offen, wie darauf sinnvoll zu reagieren ist. Was sollten wir bei einem LinkageError tun? Einen OutOfMemoryError in bestimmten Programmteilen aufzufangen, kann jedoch von Vorteil sein. Eigene Unterklassen von Error sollten keine Anwendung finden. Glücklicherweise sind die Klassen aber nur Unterklassen von Throwable und nicht von Exception, sodass ein catch(Exception e) nicht aus Versehen Dinge wie ThreadDeath abfängt, die eigentlich nicht behandelt gehören.