Die Programmiersprache Java verfügt – wie viele andere auch – über integrierte Werkzeuge zur Arbeit mit Fehlern, d. h. mit außergewöhnlichen Situationen (Exceptions), bei denen ein Programmfehler durch speziellen Code behandelt wird, der vom Basisalgorithmus getrennt ist.
Dank der Ausnahmen kann ein Entwickler Schwachstellen im Code vorhersagen und schwerwiegende Laufzeitfehler im Voraus verhindern.
Daher ist die Behandlung von Ausnahmen in Java eine gute Praxis, die die allgemeine Zuverlässigkeit des Codes verbessert.
Ziel dieses Artikels ist es, die Prinzipien des Erfassens und Behandelns von Ausnahmen zu erläutern sowie die entsprechenden Sprachstrukturen zu betrachten, die dafür vorgesehen sind.
Alle in dieser Anleitung gezeigten Beispiele wurden auf Ubuntu 22.04, installiert auf einem Cloud-Server von Hostman, ausgeführt.
Die in dieser Anleitung gezeigten Beispiele wurden mit OpenJDK ausgeführt. Die Installation ist unkompliziert.
Zuerst wird die Liste der verfügbaren Repositories aktualisiert:
sudo apt update
Anschließend kann die Liste der für den Download verfügbaren OpenJDK-Versionen abgefragt werden:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
Im Terminal wird eine kurze Liste angezeigt:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Wir verwenden die Version openjdk-21-jdk:
sudo apt install openjdk-21-jdk
Anschließend können Sie prüfen, ob Java korrekt installiert wurde, indem Sie die Version abfragen:
java --version
Die Terminalausgabe sieht etwa wie folgt aus:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Wie zu sehen ist, lautet die exakte Version von OpenJDK 21.0.5.
Alle in dieser Anleitung gezeigten Beispiele sollten in einer separaten Datei mit der Erweiterung .java gespeichert werden:
nano App.java
Füllen Sie anschließend die erstellte Datei mit einem Beispielcode wie diesem:
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Beachten Sie, dass der Klassenname mit dem Dateinamen übereinstimmen muss.
Kompilieren Sie danach die Datei:
javac App.java
Und führen Sie sie aus:
java App
Das Terminal zeigt Folgendes an:
This text is printed to the console
Alle Ausnahmen in Java haben einen bestimmten Typ, der mit dem Grund der Ausnahme verbunden ist – also der Art des Programmfehlers.
Es gibt zwei grundlegende Arten von Ausnahmen:
Der Typ Error wird nur bedingt als Ausnahme betrachtet – es handelt sich um einen vollwertigen Fehler, der das Programm unvermeidlich zum Absturz bringt.
Ausnahmen, die mit benutzerdefiniertem Code behandelt werden können und die Fortsetzung der Programmausführung ermöglichen, sind Checked Exceptions und Unchecked Exceptions.
Daher sind Fehler und Ausnahmen in Java unterschiedliche Entitäten. Beide – sowohl Errors als auch Exceptions (Checked und Unchecked) – sind Typen mit zusätzlichen Subtypen, die den Grund des Fehlers näher beschreiben.
Hier ein Beispielcode, der eine Kompilierungszeit-Ausnahme auslöst:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // Datei-Referenz erstellen
Scanner scanner = new Scanner(someFile); // Dateiinhalte parsen
}
}
Die Kompilierung wird unterbrochen, und im Terminal erscheint folgende Fehlermeldung:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Wenn Sie diese Ausnahme erfassen und behandeln, wird der Code kompilierbar und ausführbar.
Hier ein weiteres Beispiel, das eine Ausnahme erst zur Laufzeit auslöst:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // Array mit 5 Elementen erstellen
System.out.println(someArray[10]); // Versuch, auf ein nicht vorhandenes Element zuzugreifen
}
}
Während der Kompilierung tritt keine Ausnahme auf, aber nach dem Ausführen des kompilierten Codes erscheint im Terminal folgende Fehlermeldung:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Das bedeutet, dass eine solche Ausnahme durch benutzerdefinierten Code behandelt werden kann, sodass das Programm seine Ausführung fortsetzen kann.
Schließlich ein Beispielcode, der einen Laufzeitfehler verursacht:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // Stack Overflow auslösen
}
}
Die Kompilierung verläuft erfolgreich, aber während der Ausführung zeigt das Terminal einen StackOverflowError:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
In diesem Fall kann der Fehler nicht behandelt werden; er kann nur durch Korrektur des Codes beseitigt werden.
Intern werden alle Ausnahmen (und Fehler) in Java als eine Reihe von Klassen dargestellt, von denen einige von anderen erben.
Die Basisklasse für alle Fehler und Ausnahmen ist Throwable. Zwei weitere Klassen erben von ihr – Error und Exception, die als Basisklassen für eine Vielzahl von Unterklassen dienen, die mit bestimmten Ausnahmearten verbunden sind.
Die Klasse Error beschreibt Ausnahmearten des Typs Fehler, wie bereits erwähnt, während die Klasse Exception geprüfte Ausnahmen beschreibt.
Darüber hinaus erbt die Klasse RuntimeException von Exception und beschreibt ungeprüfte Ausnahmen.
Eine vereinfachte Klassenhierarchie der Java-Ausnahmen kann als folgende verschachtelte Liste dargestellt werden:
Jede Ausnahme-Klasse enthält Methoden, um zusätzliche Informationen über den Fehler abzurufen.
Die vollständige Klassifizierung der Java-Ausnahmen, einschließlich zusätzlicher Pakete, finden Sie in einer speziellen Referenzübersicht.
Alle Ausnahmen werden mithilfe spezieller try- und catch-Blöcke behandelt, die in den meisten Programmiersprachen, einschließlich Java, standardmäßig vorhanden sind.
Im try-Block schreiben Sie Code, der möglicherweise einen Fehler enthält, der eine Ausnahme auslösen kann.
Im catch-Block schreiben Sie Code, der die Ausnahme behandelt, die im vorher definierten try-Block aufgetreten ist.
Zum Beispiel kann eine try-catch-Struktur so aussehen:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
Die Ausgabe dieses Codes in der Konsole lautet:
Actually, you can't divide by zero...
Dieses Beispiel basiert auf einer illegalen Division durch null, die im try-Block eingeschlossen ist und eine ArithmeticException auslöst.
Im catch-Block wird diese Ausnahme durch die Ausgabe einer Fehlermeldung in der Konsole behandelt.
Dank dieser Struktur kann das Programm auch nach einem Fehler während der Division durch null weiterlaufen.
Im Gegensatz zu vielen anderen Programmiersprachen enthält Java einen speziellen finally-Block als Teil des Ausnahmebehandlungssystems. Er wird immer ausgeführt – unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht.
Wir können also die zuvor gezeigte Struktur erweitern:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
Nach dem Ausführen dieses Codes zeigt die Konsole:
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
Um den praktischen Nutzen des finally-Blocks zu verstehen, betrachten Sie das folgende Beispiel:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
In einem Programm mit dieser Struktur wird die Funktion hideLoaderUI() nie ausgeführt, wenn eine Ausnahme auftritt.
Man könnte versuchen, hideLoaderUI() sowohl im Ausnahmefall als auch danach aufzurufen:
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicate
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // duplicate
Dies führt jedoch zu unerwünschter Duplizierung des Funktionsaufrufs. Außerdem kann es sich bei diesem Codeblock um eine größere Logik handeln, und deren Verdopplung gilt als schlechte Praxis.
Um sicherzustellen, dass hideLoaderUI() in jedem Fall ausgeführt wird, kann der finally-Block verwendet werden:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// the loading indicator will be hidden in any case
hideLoaderUI();
}
Java ermöglicht es, Ausnahmen manuell zu erzeugen (werfen) mithilfe des speziellen Operators throw:
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
Man kann sogar eine Variable für die Ausnahme im Voraus erstellen und sie später auslösen:
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Ein weiteres wichtiges Schlüsselwort, throws (mit „s“ am Ende), ermöglicht die explizite Deklaration der Ausnahmearten (in Form von Klassennamen), die eine Methode auslösen kann.
Wenn eine solche Methode eine Ausnahme auslöst, wird sie an den aufrufenden Code weitergegeben, der sie behandeln muss:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
Die Konsolenausgabe lautet:
Dividing by zero again? Do you even know what insanity is?
Die hierarchische Struktur von Ausnahmen erlaubt es natürlich, eigene Ausnahmeklassen zu erstellen, die von den Basisklassen erben.
Dank benutzerdefinierter Ausnahmen ermöglicht Java die Implementierung von anwendungsspezifischen Fehlerbehandlungspfaden.
Zusätzlich zu den Standardausnahmen von Java können also eigene hinzugefügt werden.
Jede benutzerdefinierte Ausnahme kann wie jede vordefinierte Ausnahme mit den Standardblöcken try-catch-finally behandelt werden:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // calls the parent class constructor
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
Konsolenausgabe:
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
Diese Anleitung hat anhand von Beispielen gezeigt, warum Ausnahmen in Java benötigt werden, wie sie entstehen (einschließlich des manuellen Auslösens) und wie sie mithilfe der entsprechenden Sprachwerkzeuge behandelt werden.
Behandelbare Ausnahmen gibt es in zwei Typen:
Checked Exceptions: werden zur Kompilierungszeit behandelt.
Unchecked Exceptions: werden zur Laufzeit behandelt.
Darüber hinaus gibt es schwerwiegende Fehler, die nur durch Codekorrekturen behoben werden können:
Errors: können nicht behandelt werden.
Zur Behandlung von Ausnahmen werden mehrere Syntaxstrukturen (Blöcke) verwendet:
try: Code, der eine Ausnahme auslösen kann.
catch: Code, der die mögliche Ausnahme behandelt.
finally: Code, der immer ausgeführt wird – unabhängig vom Auftreten einer Ausnahme.
Außerdem gibt es Schlüsselwörter zur Steuerung des Ausnahmeauslösungsprozesses:
throw: löst eine Ausnahme manuell aus.
throws: listet mögliche Ausnahmen in einer deklarierten Methode auf.
Die vollständige Liste der Methoden der Elternklasse Exception finden Sie in der offiziellen Dokumentation von Oracle.