Tech Archive Help

15 Java – Exceptions

Exceptions sind Ereignisse, die während der Programmausführung auftreten und den normalen Ablauf des Programms unterbrechen. Sie werden in der Regel durch Fehler oder unerwartete Bedingungen verursacht, die vom Programm erkannt und behandelt werden können/müssen.

In den bisherigen Kapiteln ist es immer mal wieder vorgekommen, dass uns der Compiler bestimmte Exceptions um die Ohren gehauen hat. Nun werden wir uns unter anderem damit beschäftigen, wie Exceptions abgefangen werden können, was der Unterschied zwischen Checked und Unchecked Exceptions ist und wie wir eigene Exceptions auslösen oder erstellen können.

Java unterscheidet zwischen zwei Hauptarten von Exceptions: Checked Exceptions, die während der Kompilierung überprüft und vom Entwickler behandelt werden müssen und Unchecked Exceptions, die zur Laufzeit auftreten und in der Regel auf Programmierfehler hinweisen.

Checked vs. Unchecked Exceptions

Checked Exceptions

Checked Exceptions sind Ausnahmen, die zur Kompilierungszeit (Compile-Time) überprüft werden. Der Compiler muss sicherstellen, dass diese Exceptions entweder mit einem try-catch-Block abgefangen oder explizit mit dem Keyword throws weitergereicht werden.

Folgendes Beispiel zeigt eine Methodensignatur, welche mit dem Keyword throws versehen ist.

public void readFile(String path) throws FileNotFoundException { // ... }

Die Methode versucht eine Datei einzulesen. Existiert diese jedoch nicht, wird eine FileNotFoundException ausgelöst, die jedoch an den Aufrufer dieser Methode weitergereicht wird. Dort muss diese dann abgefangen werden – oder ebenfalls weitergereicht werden.

Typische Beispiele für Checked Exceptions sind IOException, die beim Arbeiten mit Dateien oder eine SQLException, die in Verbindung mit Datenbanken auftreten können. Diese Exceptions zwingen den Entwickler, sich mit potenziellen Fehlerquellen auseinanderzusetzen und eine entsprechende Fehlerbehandlung zu implementieren.

Unchecked Exceptions

Unchecked Exceptions werden zur Laufzeit des Programms geworfen, daher werden sie auch als Laufzeitausnahmen (Runtime Exceptions) bezeichnet. Sie leiten sich von der Klasse RuntimeException ab und umfassen häufige Fehler wie NullPointerException, ArrayIndexOutOfBoundsException oder ArithmeticException, welchen ihr mit Sicherheit schon mal begegnet seid.

Eine RuntimeException muss nicht zwingend behandelt werden. Da solche Fehler jedoch oft auf Programmierfehler zurückzuführen sind, sollten Entwickler den Code so schreiben, dass sie vermieden werden. Unchecked Exceptions dienen somit eher als Hinweis auf Logikfehler im Programmcode, die durch Korrekturen behoben werden können.

Throwable-Klassenhierarchie

Im Folgenden ist ein "kleiner" Teil aus der Throwable-Klassenhierarchie abgebildet. Sämtliche Exceptions und Errors stammen von Throwable ab und Throwable natürlich von Object.

15_java_exception_1.png

Exceptions vs. Errors

Ein Error repräsentiert ein schwerwiegendes Problem, welches außerhalb der Kontrolle des Programms liegt und in der Regel nicht durch den Code behoben werden kann. Er resultiert oft aus Ressourcenmangel (wie einem Speicherüberlauf → OutOfMemoryError) oder anderen kritischen Fehlern im Laufzeitsystem (z. B. einem Stapelüberlauf → StackOverflowError).

Sowohl Exception als auch Error erben beide von Throwable. Error wird jedoch meistens von der JVM in einem schwerwiegenden Zustand ausgelöst und für die Anwendung keine Möglichkeit besteht, den Fehler zu beheben.

Obwohl auch Anwendungen einen Error auslösen können, ist es keine passende Vorgehensweise, solche Fehler abzufangen, da es häufig nicht möglich ist, den Programmablauf sinnvoll fortzusetzen. Stattdessen sollten Checked Exceptions für wiederherstellbare Bedingungen und RuntimeException für Programmierfehler verwendet werden, während Error als Hinweis darauf betrachtet werden sollte, dass das Programm beendet werden oder es drastisch umstrukturiert werden muss.

Exceptions abfangen

Um Exceptions abzufangen und angemessen auf diese reagieren zu können, verwenden wir den try-catch-Block bei bestimmten Programmcodes, welche potenziell Exceptions werfen können.

try { // Code that can throw an exception. } catch (Exception e) { // Code that is executed to respond appropriately to an exception. }

Dazu folgendes Beispiel.

public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.print("Enter the numerator: "); int numerator = scanner.nextInt(); System.out.print("Enter the denominator: "); int denominator = scanner.nextInt(); int result = numerator / denominator; // Possible division by zero System.out.println("The result is: " + result); } catch (ArithmeticException e) { System.err.println(e + ": Division by zero is not allowed."); } }

Der Code kann eine ArithmeticException auslösen, sollte der Benutzer eine 0 als Nenner eingeben. Ist dies der Fall und es wird numerator / denominator ausgeführt, wird der catch-Block ausgeführt, der diese Exception abfängt und eine einfache Fehlermeldung auf der Konsole ausgibt.

Wer bereits einen Scanner für Konsoleneingaben verwendet hat oder sich Kapitel 04 – Ein- und Ausgaben angeschaut hat, wird vielleicht merken, dass die Methoden des Scanners ebenfalls eine Exception auslösen können.

Eine InputMismatchEception wird geworfen, sobald der Benutzer etwas anderes als eine Zahl eingibt oder wenn sich die Zahl außerhalb des darstellbaren Zahlenbereichs von int befindet. Auch diese Exception können wir abfangen, indem wir einen weiteren catch-Block hinzufügen.

public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.print("Enter the numerator: "); int numerator = scanner.nextInt(); System.out.print("Enter the denominator: "); int denominator = scanner.nextInt(); int result = numerator / denominator; // Possible division by zero System.out.println("The result is: " + result); } catch (ArithmeticException e) { System.err.println(e + ": Division by zero is not allowed."); } catch (InputMismatchException e) { System.err.println(e + ": Invalid input. Please enter an integer."); } }

Eine Alternative zum Abfangen der ArithmeticException kann auch eine einfache if-Abfrage sein, die überprüft, ob die Eingabe ungleich 0 ist.

if (denominator != 0) { int result = numerator / denominator; System.out.println("The result is: " + result); }

Wird genauer in die Java-Dokumentation geschaut, fällt auf, dass die Methode nextInt() nicht nur eine ArithmeticException, sondern noch zwei weitere Exceptions NoSuchElementException und IllegalStateException werfen kann.

Statt jedoch weitere catch-Blöcke hinzuzufügen, können wir einen für alle drei Exceptions schreiben.

try { // ... } catch (InputMismatchException | NoSuchElementException | IllegalStateException e) { System.err.println(e + ": Invalid input. Please enter an integer."); }

Eine InputMismatchException erbt von NoSuchElementException. Daher können wir InputMismatchException auch weglassen. NoSuchElementException und IllegalStateException erben wiederum von RuntimeException. Daher können wir diese beiden durch RuntimeException ersetzen.

try { // ... } catch (RuntimeException e) { System.err.println(e + ": Invalid input. Please enter an integer."); }

Wie wir bereits wissen, wird im Allgemeinen empfohlen RuntimeExceptions nicht abzufangen, da sie häufig auf Programmierfehler hinweisen.

Auf NoSuchElementException und IllegalStateException trifft das zu. Eine InputMismatchException kann jedoch durch einen Benutzer ausgelöst werden, wenn dieser eine ungültige Eingabe tätigt. Dieser Fall liegt außerhalb unseres Einflussbereichs, weshalb er programmatisch behandelt werden muss.

public class Main { private static final Scanner SCANNER = new Scanner(System.in); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int numerator = getValidInteger("Enter the numerator: "); int denominator = getValidInteger("Enter the denominator: "); if (denominator != 0) { int result = numerator / denominator; System.out.println("The result is: " + result); } else { System.out.println("Division by zero is not allowed."); } SCANNER.close(); } private static int getValidInteger(String text) { try { System.out.print(text); return SCANNER.nextInt(); } catch (InputMismatchException e) { System.err.println(e + ": Invalid input. Please enter an integer."); SCANNER.nextLine(); return getValidInteger(text); } } }

Die Methode getValidInteger() besitzt eine rekursive Implementierung. Der Benutzer wird so lange nach einer Ganzzahl gefragt, bis er einen gültigen Wert eingibt.

Tatsächlich ist dies ein "Es-kommt-drauf-an"-Fall. In einfachen Konsolenanwendungen spielen übermäßige Eingaben bei Rekursionen kaum eine Rolle. Insbesondere dann, wenn es nur darum geht, dass der Nutzer daran gehindert werden soll, ungültige Eingaben zu tätigen.

Die Implementierung der getValidInteger()-Methode ist eine Variante, welche ich selbst in vielen meiner Konsolenanwendungen verwende. Anstelle eines Scanners nutze ich jedoch einen BufferedReader.

Der finally-Block

Zuvor hatten wir uns angeschaut, wie wir Exceptions abfangen können. Es kann jedoch der Fall sein, dass unabhängig davon, ob eine Exception geworfen wurde oder nicht, "Aufräumarbeiten" stattfinden sollen. Z. B. wenn eine bestehende Ressource, wie ein Scanner, geschlossen werden soll.

Um dies zu erreichen, wird der finally-Block nach einem catch-Block verwendet. Dieser Block wird immer ausgeführt. Egal, ob eine Exception ausgelöst wurde oder nicht.

public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.print("Enter the numerator: "); int numerator = scanner.nextInt(); System.out.print("Enter the denominator: "); int denominator = scanner.nextInt(); int result = numerator / denominator; // Possible division by zero System.out.println("The result is: " + result); } catch (ArithmeticException e) { System.err.println("Error: Division by zero is not allowed."); } catch (IOException e) { System.err.println("Error: Invalid input. Please enter an integer."); } finally { scanner.close(); } }

Welche Zahl wird auf der Konsole ausgegeben?

public static void main(String[] main) { System.out.println(getNumber()); } public static int getNumber() { try { return 1; } catch (Exception e) { return 2; } finally { return 3; } }
3

Der finally-Block wird immer ausgeführt. Das bedeutet, dass eine return-Anweisung im finally-Block, die return-Anweisungen im try - und catch-Block überschreibt.

In den meisten Fällen werden finally-Blöcke nicht benötigt und wenn doch, dann sollte auf try-with-resources-Blöcke zurückgegriffen werden.

try-with-resources

try-with-resources ist eine spezielle Form des try-Blocks, die eingeführt wurde, um Ressourcen wie Dateien, Datenbankverbindungen, Netzwerkverbindungen oder andere Objekte automatisch zu schließen. Es bietet eine Alternative zum finally-Block und schließt am Ende des catch-Blocks automatisch die Ressource, unabhängig davon, ob eine Exception geworfen wurde oder nicht.

public void readFile(String path) { try (BufferedReader reader = new BufferedReader(new FileReader(path))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.err.println("Error reading file: " + e.getMessage()); } }

Exceptions werfen

Exceptions werden geworfen, wenn ein unerwarteter Zustand oder ein Fehler auftritt, der vom aktuellen Codeabschnitt nicht direkt behoben werden kann. Das Werfen einer Exception mittels throw ermöglicht es, das Problem an den Aufrufer weiterzuleiten, der entweder die Exception mittels try-catchbehandelt oder sie weiter nach oben im Aufrufstapel propagieren lässt.

Nehmen wir unsere Dog-Klasse aus den letzten Kapiteln als Beispiel. Der Konstruktor erwartet ein Alter als Parameter. Ein negatives Alter wäre ungültig, also könnte eine IllegalArgumentException geworfen werden.

public Dog(String name, int age) { if (age < 0) { throw new IllegalArgumentException("Age cannot be negative"); } this.name = name; this.age = age; }

Eigene Exceptions erstellen

In manchen Situationen ist es nötig, eine spezifische Exception zu haben, die einen Fehler genauer beschreibt. In solchen Fällen kann eine eigene Exception-Klasse erstellt werden und diese werfen.

public class InvalidAgeException extends Exception { public InvalidAgeException(String message) { super(message); } } public class Dog { // ... public Dog(String name, int age) { if (age < 0) { throw new InvalidAgeException("Age cannot be negative"); } this.name = name; this.age = age; } // ... }
Last modified: 07 October 2024