W3docs

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:

  1. Sie in der Regel bedeuten, dass die JVM nicht mehr zuverlässig ist. Nach einem OutOfMemoryError weiterzumachen funktioniert selten lange.
  2. Es fast nie eine sinnvolle Wiederherstellungsmaßnahme gibt, die Ihr Code ergreifen kann.
  3. 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 nicht RuntimeException sind, sind geprüft.
  • RuntimeException und 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 null
  • IllegalArgumentException — ungültiges Argument
  • IllegalStateException — falscher Zustand für die Operation
  • IndexOutOfBoundsException — Listen-/Array-Index außerhalb des Bereichs
  • ArithmeticException — Division durch null
  • ClassCastException — ungültige Typumwandlung
  • UnsupportedOperationException — 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't

Die 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 Error endet, liegt fast sicher unter Error.
  • Eine Klasse, die auf Exception endet, liegt fast sicher unter Exception — prüfen Sie jedoch, ob sie RuntimeException erweitert, 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.

java— editable, runs on the server

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:

Übungen

Übung
Ein Programm verwendet `catch (Exception e)` um einen Block. Was davon wird abgefangen?
Ein Programm verwendet `catch (Exception e)` um einen Block. Was davon wird abgefangen?
Was this page helpful?