Blog

Erstklassige Fehlerszenarien in Java

Matthisk Heimensen

Aktualisiert Oktober 21, 2025
5 Minuten

Geprüfte Ausnahmen waren ein Versuch der Java-Entwickler, die Möglichkeit eines Fehlers in der Typsignatur auszudrücken, um den Benutzern dieser Methoden zu helfen, das Fehlerszenario anständig zu behandeln. Obwohl die Absichten des Designs edel waren, hat sich das Endergebnis nicht wie erwartet entwickelt. Die meisten Java-Programmierer haben geprüfte Ausnahmen zugunsten ihrer ungeprüften Gegenstücke aufgegeben. Das heutige Java ermöglicht es uns, Fehler in der Typsignatur auf neue und bessere Weise auszudrücken. In diesem Beitrag erkläre ich, warum geprüfte Ausnahmen in Ungnade gefallen sind und welchen besseren Ansatz es gibt, um die Möglichkeit eines Fehlers auszudrücken.

Das Problem

Geprüfte Ausnahmen ermöglichen es uns, mögliche Fehler in der Typsignatur unserer Methoden auszudrücken. Nehmen wir die folgende Methode, die eine Datei von der Festplatte liest:

String readFile(String filename) throws IOException

Jede Aufrufsite dieser API ist nun gezwungen, explizit die Möglichkeit zu behandeln, dass die angeforderte Datei nicht existiert. Wir ermöglichen es dem Compiler, dem Benutzer unserer API zu helfen. So weit, so gut.

Eine Anwendung in der realen Welt besteht jedoch nicht aus einer einzigen Methode, sondern aus der Komposition vieler Methoden zu einem größeren Verhalten. Wir möchten zum Beispiel eine Entität aus der Datei, die wir gerade von der Festplatte gelesen haben, parsen:

Entity parseEntity(String input) throw JsonParseException

Jetzt setzen wir das Ergebnis unserer Funktion zum Lesen einer Datei mit unserer Funktion zum Parsen einer Entität zusammen:

parseEntity(readFile("entity.json"))

Geprüfte Ausnahmen sind in der Lage, diese Zusammensetzung zu verarbeiten. Dazu müssen wir diese Zeile in einen try-catch-Block mit zwei separaten catch-Blöcken verpacken, einen für jeden Ausnahmetyp:

try {
  parseEntity(readFile("entity.json"))
} catch (IOException e) {
  /* handle IOException */
} catch (JsonParseException e) {
  /* handle JsonParseException */
}

Es ist zwar nicht besonders elegant, aber es ist möglich, die Komposition dieser beiden Methoden unter Verwendung geprüfter Ausnahmen auszudrücken. Lassen Sie uns unser Beispiel weiter ausbauen. Was wäre, wenn wir eine Liste von Dateinamen von der Festplatte lesen und jeden als Entität analysieren müssten. Mit Java 8+ würden wir dies als eine Map-Operation über einen Stream modellieren:

streamOfFiles.map(this::readFile).map(this::parseEntity)

Dieses Beispiel lässt sich nicht kompilieren, weil die Map-Operation keine Methoden verarbeiten kann, die geprüfte Ausnahmen auslösen. Dies wird deutlich, wenn wir uns die (vereinfachte) Typsignatur der Methode map the ansehen:

Stream<?> map(Function<?, ?> mapper)

Indem wir das Fehlerszenario in die Aufrufsignatur der Methode aufgenommen haben, haben wir unsere Fähigkeit beeinträchtigt, diese Methode als Baustein für größere, komplexere Verhaltensweisen wiederzuverwenden.

Die Lösung

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, unsere Methoden so zu konvertieren, dass sie ungeprüfte Ausnahmen auslösen. Auf diese Weise können wir unsere Methode per Referenz an andere Methoden weitergeben, die sie ohne Beschwerden des Compilers aufrufen können. Indem wir das Fehlerszenario aus unserer Typsignatur entfernt haben, haben wir dafür gesorgt, dass der Compiler den Benutzern dieser API nicht mehr helfen kann, die Möglichkeit eines Fehlers explizit zu behandeln.

Wie wäre es, wenn wir, anstatt uns auf die spezielle Syntax und Semantik von geprüften Ausnahmen zu verlassen, das Fehlerszenario als Teil der Standardsignatur mitteilen könnten. Wir könnten dies erreichen, indem wir von unserer Funktion einen Containerwert zurückgeben, der entweder einen Fehler oder einen Wert darstellt:

Either<Exception, String> readFile(String filename)

Der Compiler kann diesen Wert verwenden, um den Benutzer unserer API anzuleiten, das Szenario eines Fehlers explizit zu behandeln.

Durch Hinzufügen der Operationen map und flatMap für diesen Containertyp können wir andere Methoden zusammenstellen, die entweder einen Wert oder einen neuen Container über den Wertteil des Containers Either zurückgeben:

readFile("entity.json").flatMap(this::parseEntity)

Das Beispiel für das Parsen mehrerer Dateien kann auch mit unserem either-Containerwert ausgedrückt werden. Dazu müssen wir flatMap auf die either-Werte anwenden, die Einträge in unserer Liste sind:

streamOfFiles
  .map(this::readFile)
  .map(either -> either.flatMap(this::parseEntity))

Die Operationen map und flatMap für den Container Entweder werden nur aufgerufen, wenn der Zielcontainer einen Wert enthält. Wenn also readFile fehlschlägt, wird parseEntity nicht aufgerufen und die IOException von readFile blubbert den Stream hoch.

Dadurch, dass wir Fehlerszenarien als allgemeine Werte darstellen, anstatt uns auf eine spezielle Semantik zu verlassen. Wir können Kombinatoren (d.h. Methoden, die nur auf ihre Eingabe wirken) schreiben, die das allgemeine Verhalten kapseln, das wir beim Umgang mit Fehlerszenarien ausdrücken möchten.

Wir könnten zum Beispiel eine Methode schreiben, die die Nachricht aus unserem Fehlerszenario auspackt:

<R> Either<String, R> excToString(Either<Exception, R> either) {
  return either.mapLeft(Throwable::getMessage);
}

Oder wir könnten einen Kombinator schreiben, der den ersten Either zurückgibt, wenn er einen Wert enthält. Wenn er aber einen Fehler enthält, geben wir den zweiten Either zurück:

<L, R> Either<L, R> alternative(Either<L, R> first, Either<L, R> second) {
  return first.fold(l -> second, Either::right);
}

Dies beruht auf der Methode Either.fold, die die linke Seite oder die rechte Seite der Disjunktion faltet. Ihre API sieht in etwa so aus:

<L, R, U> U fold(Function<L, U> leftMapper, Function<R, U> rightMapper)

Wir können diesen Kombinator verwenden, um einen Ausweichwert zu verwenden, wenn unsere Datei entity_X.json nicht existiert:

alternative(readFile("entity_X.json"), readFile("default_entity.json"));

(Beachten Sie, dass Argumente, die an alternative übergeben werden, nicht träge interpretiert werden. Daher wird dieser Ausdruck versuchen, beide Dateien zu laden, obwohl nur 1 verwendet wird)

Fazit

Ähnlich wie Sie Funktionen zu Bürgern erster Klasse machen, können Sie Fehlerszenarien zu Bürgern erster Klasse machen, indem Sie Code schreiben, der von diesen Problemen abstrahiert und sie zu neuen Verhaltensweisen kombiniert. Typsichere explizite Fehlerszenarien bieten Ihnen sowohl die Vorteile von geprüften Ausnahmen, d.h. sie machen Fehlerszenarien für den Benutzer explizit, als auch die Kompositionsfähigkeit von Methoden, die ungeprüfte Ausnahmen auslösen. Um mit der Programmierung unter Verwendung des Typs Either zu beginnen, können Sie die VAVR-Sammlungsbibliothek verwenden, die Either neben vielen anderen nützlichen, von der funktionalen Programmierung inspirierten Sammel- und Containertypen enthält. Wenn Sie sich von den Ideen in diesem Beitrag angesprochen fühlen und mehr erfahren möchten, kann ich Ihnen den Vortrag "Railway Oriented Progamming" empfehlen.

Verfasst von

Matthisk Heimensen

A hands-on engineering consultant developing software across the stack. Helping teams deliver more predictably with more fun.

Contact

Let’s discuss how we can support your journey.