20.12.20 2690 Views 32

credit: ©MiaStendal

Java Tutorial #37

Exception Handling

In einem laufenden Programm kann viel schiefgehen. Denn auch wenn wir einen compilierbaren und (scheinbar) fehlerfreien Code geschrieben haben, kann das Programm zur Laufzeit abschmieren. Was wir dagegen tun können und wie genau das funktioniert, ist Aufgabe des Exception Handlings.

Falconbyte unterstüzen

Betrieb und Pflege von Falconbyte brauchen viel Zeit und Geld. Um dir auch weiterhin hochwertigen Content anbieten zu können, kannst du uns sehr gerne mit einem kleinen "Trinkgeld" unterstützen.

Thema in Kurzform

try{
    // riskante Methode
}
catch(Exception e){ // Exception auffangen
    // auszuführender Code
}
finally{
    // wird in jedem Fall ausgeführt (optional)
}
  • Exceptions sind Ausnahmesituationen zur Laufzeit, die unbehandelt das Programm zum Absturz bringen können.
  • Die Behandlung einer Ausnahmesituation zur Vermeidung des Programmabsturzes wird Exception Handling genannt.
  • Zwar ist dies mti Aufwand verbunden, doch für die Entwicklung stabiler Programme notwendig.

    Inhaltsverzeichnis

  1. Die Grundlagen
  2. Exception-Handling
  3. Eine Exception ist ein Objekt
  4. finally
  5. Übungen

Die Grundlagen

In einem laufenden Programm kann viel schiefgehen. Denn auch wenn wir einen compilierbaren und (scheinbar) fehlerfreien Code geschrieben haben, kann das Programm zur Laufzeit abschmieren.

Ein solcher Programmabsturz kann beispielsweise durch folgende "Ausnahmesituationen" (engl: Exceptions) eintreten:

  • Der Server ist nicht erreichbar.
  • Eine Datei kann nicht gefunden werden oder ist nicht beschreibar.
  • Es gibt ein Problem bei der Datenbankverbindung.
  • Es wird versucht, eine Methode auf einer Null-Referenz aufzurufen.
  • Der Endnutzer des Programms macht eine nicht verarbeitbare Eingabe in ein Eingabefenster.
  • usw...

Jeder dieser "Ausnahmesituationen" stellt eine Störung im Programmablauf dar. Das Programm kann diese Störung nicht ohne Weiteres verarbeiten und wird abstürzen.

Wir haben einige Exceptions auch schon konkret in früheren Kapiteln kennengelernt: Zum Beispiel die ArrayIndexOutOfBoundsException bei Überschreitung gültiger Array-Grenzen oder die ClassCastException beim Scheitern der Typumwandlung.

Diese letztgenannten Exceptions sind oftmals auf Programmierfehler zurückzuführen. Aber wie sieht es beim spontanen Verlust der Datenbankverbindung oder einem Zusammenbruch eines externen Web-Servers aus? Ist der Programmierer bei Programmabstürzen aus Gründen, die außerhalb seines Einflusses liegen, dann "unschuldig"?

Nein, denn auch in diesen Situationen sollten wir "Verantwortung" dafür tragen, dass das Programm weiterlaufen kann (defensive Programmierung). Zwar können wir nicht immer voraussehen, wann eine bestimmte Ausnahme zur Laufzeit eintritt. Doch sollten wir das Programm vorausschauend so entwickeln, um im "Falle eines Falles" vorbereitet zu sein. Das heißt: Wir sollten die riskanten Methoden unseres Programms kennen, die es zur Laufzeit zum Crash bringen könnten und entsprechende Vorkehrungen treffen, damit es in keinem Fall dazu kommt.

Das "Behandeln" einer "Ausnahmesituation" wird Exception Handling genannt. Diese Technik garantiert das Weiterlaufen des Programms im Falle einer Exception zur Laufzeit und sorgt dafür, dass sich die Benutzer selbst in Fehlersituationen auf unsere Software verlassen können.

Wie genau das funktioniert, sehen wir uns nun an.

Exception-Handling

Greifen wir das Beispiel einer unerwünschten Benutzereingabe einmal auf. Stellen Sie sich vor, ein Benutzer Ihres Programms soll einen Geldbetrag in Euro eingeben. Dies könnte über ein Eingabefenster am Bildschirm erfolgen. Da wir Benutzeroberflächen erst später kennenlernen, setzen wir die Benutzereingabe daher mit den uns bekannten Mitteln der Konsoleneingabe via Scanner-Klasse um:

import java.util.Scanner;

public class BetragEingeben {

    public static void main(String[] args){

        Scanner scanner = new Scanner(System.in);

        System.out.println("Betrag eingeben: ");
        double input = scanner.nextDouble(); // riskante Methode!

        System.out.println("Eingegebener Betrag " + input);

    }
}

Das kleine Programm sieht in der Konsole so aus:

Java Konsole Exception Input Scanner

Klappt alles wunderbar. Das ist aber dem Benutzer zu verdanken, der mit 1000 brav einen für die Methode scanner.nextDouble() gültigen Wert eingibt.

Bei der Eingabe von 1000.50 aber geschieht das Unheil:

Java Konsole Exception Input Scanner

Das Programm ist doch tatsächlich abgestürzt! Sehen Sie das gelbe Zeug? Das ist die Exception-Meldung. Dadurch können wir immerhin erkennen, wo das Problem lag: Exception in thread "main" java.util.InputMismatchException. Eine InputMismatchException ist also aufgetreten, d.h. eine Exception zur Laufzeit aufgrund einer fehlerhaften Eingabe. Außerdem sehen wir, dass das auf die Methode nextDouble() zurückzuführen ist. Der Aufruf der Methode ist eben riskant!

Hierzu muss man wissen, dass die Methode scanner.nextDouble() die Sprache des Betriebssystem des Benutzers erkennt und damit bei Fließkommawerten nicht den Punkt, sondern (wie im Deutschen üblich) das Komma erwartet. Bei der Eingabe 1000,50 hätte es also funktioniert.

Dennoch: Wir können es nicht zulassen, dass unser Programm bei einer fehlerhaften Benutzereingabe stirbt.

Exception auffangen

Wir kennen nun also die riskante Methode scanner.nextDouble() und wissen, dass diese eine Exception auslösen kann. Wird diese zur Laufzeit dann tatsächlich ausgelöst, führt sie ohne spezielle Behandlung zum Programmabsturz.

Treffen wir also Vorkehrungen und setzen das Exception-Handling um. Das geht grundsätzlich folgendermaßen:

Java Exception try catch

Die riskante Methode, die eine Exception auslösen könnte, wird mit einem try-Block umgeben. Wird eine Exception dann tatsächlich ausgelöst (man sagt auch "geworfen"), wird sie mit dem catch-Block aufgefangen und das Programm kann weiterlaufen.

Falls im try-Block eine Ausnahmesituation auftritt, wird die restliche Verarbeitung darin sofort beendet und es wird direkt zum catch-Block gesprungen.

Der catch-Block dient nicht nur dazu, dass eine Exception gefangen wird und das Programm weiterlaufen kann. In ihm wird auch die Fehlerbehandlung implementiert. Was genau im catch-Block geschrieben wird, hängt ganz davon ab, was im falle einer Exception geschehen soll. Es kann zum Beispiel dem Benutzer eine aufbereitete Fehlermeldung aangezeigt werden. Oder wenn der Server nicht erreichbar ist, könnte man im catch-Block einen anderen ansteuern. Sollte der Benutzer eine ungültige Eingabe gemacht haben, könnte er durch den Code im catch-Block aufgefordert werden, es noch einmal zu versuchen.

Exceptions sind in Java Objekte der Klasse Exception. Die Parameter-Variable (e) im catch-Block ist damit eine Referenz auf das aufzufangende Exception-Objekt. Was das konkret bedeutet, sehen wir uns im nächsten Abschnitt an.

Der angepasste Code in der Main-Methode sieht mit dem hinzugefügten try-catch-Block nun so aus:

Scanner scanner = new Scanner(System.in);

System.out.println("Betrag eingeben: ");
double input = 0;
try {
    input = scanner.nextDouble(); // riskante Methode
}
catch (Exception e) {
    System.out.println("Exception wurde gefangen. Programm läuft weiter.");
}

System.out.println("Eingegebener Betrag " + input);

}

Damit ist das Programm gegen einen Absturz durch eine nicht behandelte Exception abgesichert:

Vererbungshierarchie

Exceptions sind in Java Objekte der Klasse Exception. Tatsächlich gibt es eine Vielzahl verschiedener Exception-Klassen, die alle die Superklasse java.lang.Exception erweitern. Diese erbt wiederum von der Klasse Throwable. Sehen wir uns doch die Vererbungshierarchie von Exception im Kontext einmal genauer an:

Java Exception checked unchecked

Die Klasse Error

Neben Exception wird Throwable außerdem von einer Klasse namens Error erweitert.

Error zeigt schwerwiegende Probleme im laufenden Programm an, die so fatal sind, dass eine Fehlerbehebung keinen Sinn ergibt. Es handelt sich dabei um "abnormale" Ereignisse, die sich der Kontrolle des Programms entziehen. Hierzu zählen z.B. JVM-Fehler, out of memory, infinite Rekursion oder Probleme mit dem Bytecode.

Bei solchen eher selteneren Fehlern sind die Dinge bereits so katastrophal schiefgelaufen, dass wir keine "Wiederbelebung" des Programms versuchen sollten.

Damit ist auch klar, was der Unterschied der beiden von Throwable abgeleiteten Klassen ist:

  • Exception: Leichte "Fehler", die als Ausnahmesituationen im Programmablauf durch Exception-Handling zu beheben sind. Das Programm kann i.d.R. sinnvoll weiterlaufen.
  • Error: Harte Fehler. In jedem Fall sollten sie das Ende des Programms markieren.

Abgeleitete Klassen von Exception

Unterhalb der Klasse Exception wird der Stammbaum komplexer und es gibt verschiedene Exception-Typen. Wir gehen im nächsten Kapitel genauer darauf ein.

Das Exception-Objekt

Wenn wir eine Exception behandeln, verhindern wir damit nicht nur den Programmcrash, sondern wir fangen (catch!) das geworfene Exception-Objekt. Indem es im catch-Block der Parameter-Variable zugewiesen wird, können wir über diese Referenz die Methoden der Klasse Exception - die sie von Throwable erbt - aufrufen.

Rückgabetyp Methode Beschreibung
String getMessage() Liefert einen Meldungstext zur Exception
String toString() Liefert den Exception-Typ sowie den Meldungstext als String zurück. Der Exception-Text kann damit gespeichert und weiterverarbeitet werden.
void printStackTrace() Gibt den Inhalt von toString(), eine Liste der Methoden auf dem Stack mit Zeilengaben der Fehlerquelle im Code auf der Konsole wider.

Testen wir das mit der Methode printStackTrace():

int[] array = new int[3];

try {
    int x = array[3]; // wird Exception auslösen!
} catch (Exception e) { // Exception der Referenz e zuweisen
    e.printStackTrace();
}

Auf der Konsole erhalten wir eine Exception namens ArrayIndexOutOfBoundsException angezeigt, die wir von der Arbeit mit Arrays bereits kennen:

java.lang.ArrayIndexOutOfBoundsException: 3
at p1.BetragEingeben.main(BetragEingeben.java:17)

Einen solch miesen Code wie diesen sollten wir natürlich prinzipiell vermeiden. Er dient hier nur zu "Anschauungszwecken", um das Verständnis für das Exception-Objekt zu stärken. Einen Index-Wert eines Arrays anzusteuern, der außerhalb der gültigen Grenzen liegt, ist nämlich ein typischer (vermeidbarer) Programmierfehler. In der Praxis macht es keinen Sinn, hier den Exception-Handling-Mechanismus einzusetzen, zumal wir uns des Fehlers ja bewusst sind. Merken wir uns deshalb:

  • Fehlervermeidung geht vor Fehlerbehandlung!

finally

Nach dem catch-Block kann optional ein finally-Block gesetzt werden. Dieser wird immer ausgeführt, gleichgültig, ob eine Exception ausgelöst wird oder nicht:

// weiterer Code ausgelassen...
try {
    input = scanner.nextDouble(); // riskante Methode
}
catch (Exception e) {
    System.out.println("Exception wurde gefangen. Programm läuft weiter.");
}
finally {
    // wird auf jeden Fall ausgeführt
}

finally wird typischerweise eingesetzt, um einen Datenbankverbindung oder den Zugriff auf eine Datei zu schließen. Die Verbindung wird also auch dann sauber getrennt, wenn etwas schiefgegangen ist. Insofern ist finally unter bestimmten Umständen eine Frage des guten Programmierstils.

Übungen

einfach

Was ist der Unterschied zwischen Exception und Error?

Lösung ein-/ausblenden

mittel

Welchen Wert hat die Variable i am Ende des Codes?

int i = 0;
try {
    System.out.println(2 / 0);
    i++;
} catch (Exception e) {
    i++;
}
finally {
    i++;
}
Lösung ein-/ausblenden

schwer

public class Falcon {

    public Falcon f;

    public void goInline(Falcon f){
        System.out.println(1);
        try {
            System.out.println(2);
            System.out.println(f.toString());
            System.out.println(3);
        }
       finally{
            System.out.println(4);
        }
        System.out.println(5);

    }


    public static void main(String[] args){
        Falcon falcon = new Falcon();
        falcon.goInline(falcon.f);
    }

}

Multiple choice: Welche Zahlen und welcher Text werden auf der Konsole angezeigt?

A. 1 2
B. 1 2 3 4
C. 1 2 4
D. 1 2 3 4 5
E. 1 2 4 5
F. Exception in thread "main" java.lang.NullPointerException
G. Der Code lässt sich nicht compilieren, da der catch-Block fehlt.

Lösung ein-/ausblenden

Java einrichten

Erfahren Sie, wie einfach es ist, mit Java zu beginnen

Arrays in Java

Arrays ermöglichen das Speichern von Daten in einer übergeordneten Datenstruktur

Der Konstruktor

Was ist eigentlich ein Konstruktor?

FALCONBYTE.NET

Handmade with 🖤️

© 2018-2022 Stefan E. Heller

Impressum | Datenschutz | Changelog

Falconbyte Youtube Falconbyte GitHub facebook programmieren lernen twitter programmieren lernen discord programmieren lernen