Java Exception-Hierarchie
Wie Throwable, Error, Exception und RuntimeException in Javas Exception-Klassenhierarchie zusammenhängen.
Jede Exception in Java ist Teil eines kleinen Klassenbaums, dessen Wurzel java.lang.Throwable ist. Die Struktur dieses Baums zu kennen zahlt sich immer wieder aus: Sie erklärt, warum catch (Exception e) keinen OutOfMemoryError abfängt, warum RuntimeException besonders ist und warum manche Exceptions behandelt werden müssen, andere hingegen nicht. Die gesamte Struktur passt in ein einziges Diagramm.
Diese Seite zeigt den Baum im Überblick: die Wurzel Throwable, die Zweige Error und Exception, wo die Grenze zwischen geprüften und ungeprüften Exceptions liegt und wie all das steuert, was Ihre catch-Blöcke tatsächlich abfangen.
Der gesamte Baum
Throwable
├── Error (unchecked — JVM-level)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── VirtualMachineError
│ └── ...
└── Exception (checked by default)
├── IOException (checked)
├── SQLException (checked)
├── ClassNotFoundException (checked)
├── ...
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
├── IllegalStateException
└── ...Der gesamte Baum ist eine einzige Klassenhierarchie. Deshalb fängt ein catch für einen Supertyp auch dessen Subtypen ab, deshalb verhalten sich Exception-Variablen wie gewöhnliche Referenzen und deshalb können Sie eine IOException in einem Exception-typisierten Feld speichern.
Throwable — die Wurzel
Throwable ist das, was throw akzeptiert und was catch deklariert. Alles, was Sie auslösen oder behandeln möchten, ist eine Unterklasse von Throwable. Die Klasse selbst stellt Folgendes bereit:
- Eine Nachricht (
getMessage()) - Einen Stack-Trace, der bei der Erstellung erfasst wird (
getStackTrace(),printStackTrace()) - Eine optionale Ursache — ein weiteres
Throwable, das dieses ausgelöst hat (getCause()) - Unterdrückte Exceptions — sekundäre Fehler, die durch try-with-resources angehängt werden (
getSuppressed())
Sie erweitern Throwable fast nie direkt. Das interessante Design findet eine Ebene tiefer statt.
Error — nicht abfangen
Error und seine Unterklassen stellen Fehler dar, die die JVM signalisiert: Speichermangel, Stack-Überlauf, eine Klassendatei, die nicht verknüpft werden kann. Gemäß Konvention fangen Sie diese im Anwendungscode nicht ab, weil:
- Sie in der Regel bedeuten, dass die JVM nicht mehr zuverlässig ist. Nach einem
OutOfMemoryErrorweiterzumachen funktioniert selten lange. - Es fast nie eine sinnvolle Wiederherstellungsmaßnahme gibt, die Ihr Code ergreifen kann.
- Die JVM möglicherweise bereits etwas unternimmt; ein Abfangen stört dabei.
Error ist technisch gesehen abfangbar — Java hält Sie nicht davon ab. Aber die Konvention ist so stark, dass "fängt Error ab" als Code-Review-Warnsignal gilt. Der einzige legitime Anwendungsfall ist ein übergeordneter Supervisor (ein Request-Handler, ein Job-Runner), der protokolliert und sauber beendet.
Exception — Anwendungsfehler
Alles außer Error unter Throwable ist Exception oder einer seiner Subtypen. Die Grenze zwischen geprüften und ungeprüften Exceptions verläuft innerhalb dieses Zweigs, nicht darüber:
- Direkte Unterklassen von
Exception, die nichtRuntimeExceptionsind, sind geprüft. RuntimeExceptionund alle seine Subtypen sind ungeprüft.
Deshalb passt catch (Exception e) sowohl auf IOException (geprüft) als auch auf NullPointerException (ungeprüft) — sie sind Geschwister unter derselben Wurzel. Das ist auch der Grund, warum das Abfangen von Exception so grob ist: Sie fassen beide Zweige zusammen.
Die Unterscheidung zwischen geprüft und ungeprüft hat reale Konsequenzen für Ihre Methodensignaturen — geprüfte Exceptions müssen mit throws deklariert oder behandelt werden, ungeprüfte nicht. Dieser Kompromiss wird in geprüfte vs. ungeprüfte Exceptions ausführlich behandelt.
RuntimeException — der Bug-Zweig
RuntimeException und seine Subtypen sind per Konvention für Programmierfehler reserviert, die in korrektem Code nicht auftreten sollten:
NullPointerException— Dereferenzierung von nullIllegalArgumentException— ungültiges ArgumentIllegalStateException— falscher Zustand für die OperationIndexOutOfBoundsException— Listen-/Array-Index außerhalb des BereichsArithmeticException— Division durch nullClassCastException— ungültige TypumwandlungUnsupportedOperationException— Operation nicht unterstützt (z. B. Änderung einer unveränderlichen Liste)
Sie können diese von überall auslösen, ohne Ihre Methodensignatur zu ändern. Aufrufer können sie abfangen, aber die Sprache zwingt sie nicht dazu. Sie sind das richtige Werkzeug, wenn der Fehler bedeutet "das ist ein Bug" und nicht "das passiert manchmal."
Typbeziehungen in catch
Ein catch (T e) passt auf jeden ausgelösten Wert, der eine Instanz von T oder ein Subtyp von T ist. Die Hierarchie bestimmt also direkt, was Ihre Catches sehen:
try { ... }
catch (IOException e) { ... } // catches FileNotFoundException too
catch (Exception e) { ... } // catches almost everything below Throwable
catch (Throwable t) { ... } // catches everything, including Error — don'tDie Reihenfolge ist hier wichtig: Da jedes catch Subtypen abfängt, müssen Sie die spezifischsten Typen zuerst auflisten. Würde catch (Exception e) vor catch (IOException e) stehen, wäre der IOException-Block unerreichbar und der Compiler würde es ablehnen. Die vollständigen Regeln finden Sie unter mehrere catch-Blöcke.
Das ist auch der Grund, warum eine alles-in-einem-Catch-Form gefährlich ist. catch (Exception) fängt NullPointerException (ein Bug), IOException (ein behebbarer Fehler) und IllegalStateException (wahrscheinlich ein Bug) — alles in einem Block, ohne Möglichkeit, sie unterschiedlich zu behandeln. Die Hierarchie fordert Sie auf, spezifischer zu sein.
Typen nachschlagen
Wenn Sie eine neue Exception in einem Stack-Trace begegnen und wissen möchten, wo sie sich befindet:
- Sie ist in
java.lang, wenn es sich um einen grundlegenden Fehler handelt (NullPointerException,ArithmeticException). - Sie ist in
java.io,java.sql,java.net, wenn sie zum Aufgabenbereich dieses Pakets gehört. - Eine Klasse, die auf
Errorendet, liegt fast sicher unterError. - Eine Klasse, die auf
Exceptionendet, liegt fast sicher unterException— prüfen Sie jedoch, ob sieRuntimeExceptionerweitert, um zu wissen, ob sie geprüft ist.
Das Javadoc zeigt die Vererbungskette am Anfang jeder Seite. Im Zweifelsfall nachschlagen.
Ein ausgearbeitetes Beispiel
Ein kleines Programm, das die Hierarchie mit instanceof-Prüfungen durchläuft. Es fängt eine Folge von Throws als Throwable ab und meldet dann, wo jede im Baum sitzt.
Der isChecked-Helfer kodiert die Regel in einer Zeile: Die geprüfte Teilmenge ist Exception minus RuntimeException. Führen Sie das Programm aus und Sie sehen genau, wo jede der fünf Instanzen steht: IOException ist geprüft, die zwei RuntimeExceptions nicht, der OutOfMemoryError ist ein Error (also weder Exception noch geprüft), und die einfache Exception ist geprüft.
Was als Nächstes kommt
Der eingebaute Baum deckt die meisten Fälle ab. Wenn Ihre Domäne eigene Fehler hat — "Rechnung nicht gefunden", "Konfiguration veraltet" — schreiben Sie Ihre eigenen Klassen, indem Sie den richtigen Knoten in diesem Baum erweitern. Weiter zu benutzerdefinierte Java-Exceptions.
Weiterführende Lektüre:
- Try-catch-Grundlagen — wie
catchtatsächlich einen Handler auswählt. throwundthrows— Exceptions auslösen und geprüfte deklarieren.- Geprüft vs. ungeprüft — die Grenzlinie innerhalb des
Exception-Zweigs. - Best Practices für Exceptions — was mit dem zu tun ist, was Sie abfangen.