Overview
Man unterscheided zwei Gruppen vom Exceptions: Unchecked und checked Exceptions. Für die Behandlung von Ausnahmezuständen ist das Wissen über den unterschiedlichen Umgang beider Exception-Arten essentiell.
Wie wir im letzten Kapitel gesehen haben, sind Exceptions "Ausnahmezustände", die den Programmfluss stören und das Programm zum Absturz bringen können. Alle Exceptions sind Instanzen der Klasse Exception und erweitern die Superklasse Throwable.
Von Exception werden wiederum zahlreiche Subklassen abgeleitet. Sehen wir uns einen Ausschnitt der Exception-Klassenhierarchie im Gesamtkontext einmal an:
In Java werden Exceptions in zwei Gruppen unterteilt: Unchecked Exceptions und checked Exceptions. Gehen wir zuerst der Frage nach, was unchecked Exceptions sind.
Alle Klassen, die RuntimeException erweitern, sind unchecked Exceptions. Diese Exceptions treten zur Laufzeit (runtime) des Programms auf. Sie werden "unchecked" (dt. ungeprüft) genannt, weil der Compiler für diese zur Übersetzungszeit keineÜberprüfung vornimmt.
Die meisten Runtime Exceptions resultieren aus Situationen, die vermeidbar gewesen wären, d.h. sie gehen zumeist auf logische Programmierfehler zurück. Es ist nicht die Aufgabe des Compilers, Sie hierbei zu unterstützen. Deshalb werden sie in Ihrer Programmierumgebung nicht als Fehler angezeigt und der Compiler lässt sie ohne Prüfung "unter dem Radar" durchgehen. Ein compilierbares Programm ist also nicht gleich auch fehlerfrei.
Sehen wir uns nur einige unchecked Exceptions bzw. RuntimeExceptions exemplarisch etwas genauer an.
Diese Exception wird "geworfen", wenn ein Integer-Wert durch 0 geteilt wird (Division durch 0).
int part = 4 / 0; // ArithmeticExceptionWer kennt sie nicht, die berühmte ArrayIndexOutOfBoundsException? Falls wir einen Index-Wert ansprechen, der außerhalb der Array-Grenzen liegt, wird diese Exception geworden:
String[] names = new String[4];
String name = names[4]; // ArrayIndexOutOfBoundsExceptionEs gibt in diesem Array mit der Länge 4 nur die Indices 0, 1, 2, 3. Alles andere ist außerhalb der gültigen Array-Grenze.
Wenn wir versuchen, eine ungültige Typumwandlung (cast) durchzuführen, erhalten wir eine ClassCastException:
String s = "ABC";
Object o = s;
Integer i = (Integer) o; // ClassCastExceptionDer Compiler lässt diesen Code zur Übersetzungzeit durchgehen, da er hinsichtlich der statischen Situation in der dritten Zeile eine Typumwandlung von Object nach Integer sieht, was prinzipiell auch in Ordnung geht. Der Compiler kann aber nich wissen, dass Object o auf ein String-Objekt ("ABC") zeigt. Zur Laufzeit (dynamisch) wird der Code deshalb crashen, weil eine Umwandlung von String nach Integer schlicht ungültig ist (mehr zum Thema Typumwandlung).
Methoden können nicht auf einer Null-Referenz aufgerufen werden. Null-Referenzen sind Referenzvariablen, die auf kein Objekt zeigen, also den Wert null haben:
public class Falcon {
    public String f;
    public void p(){
        System.out.println(f.length());
    }
    public static void main(String[] args){
        new Falcon().p(); // NullPointerException
    }
}Der Methodenaufruf new Falcon().p(); wird eine NullPointerException auslösen, da die String-Referenz f auf kein String-Objekt zeigt (Wert = null)
Java stellt mit den Wrapper-Klassen Methoden bereit, die Strings in primitive Werte umwandeln können. Wenn dies mit ungültigen Werten versucht wird, wird eine NumberFormatException geworfen:
int x = Integer.parseInt("abc"); // NumberFormatExceptionIm letzten Kapitel haben wir noch die InputMismatchException kennengelernt, die bei einer ungültigen Konsolen-Eingabe geworfen wird. Es gibt aber noch weitaus mehr Unterklassen von RuntimeException, die alle ihren speziellen Zweck haben. Wir konnten aus Platzgründen hier nicht alle im Detail besprechen. Sicherlich werden Sie im Laufe Ihrer Programmierkarriere aber noch die ein oder andere RuntimeException kennenlernen 😀
Alle Exceptions, die nicht Unterklassen von RuntimeException sind, gehören zur Gruppe der checked Exceptions. Was bedeutet das, wenn eine Exception als "checked" definiert ist?
Checked Exceptions sind "geprüfte" Exceptions, das heißt, dass der Compiler zur Übersetzungszeit eine Prüfung in Form eines try-catch-Mechanismus vom Programmierer einfordert. Fehlt das entsprechende Exception-Handling, ist der Code nicht compilierbar. Wir müssen also bestätigen, dass wir uns des Risikos eines bestimmten Methodenaufrufs bewusst sind. Eben das ist der Unterschied zu den RuntimeExceptions: Diese werden vom Compiler ignoriert und auch ohne Exception-Handling "ungeprüft" (unchecked) durchgelassen.
Checked Exceptions sind für Fälle gedacht, in denen wir damit rechnen sollten, dass etwas schieflaufen könnte. Wenn wir zum Beispiel eine Datei schreiben wollen, können wir nicht garantieren, dass die Datei z.B. nicht schreibgeschützt ist. Das liegt außerhalb der "Macht" unseres Programms. In solchen Fällen werden wir als Programmierer vom Compiler "gezwungen", den Erfolg des riskanten Vorhabens zu überprüfen.
Zu den checked Exception gehören unter anderen:
Sehen wir uns ein Beispiel an, in dem wir es mit den beiden checked Exception-Typen FileNotFoundException und IOException zu tun bekommen könnten:
Mit dem folgenden Programm soll die erste Zeile einer Datei namens info.txt ausgelesen und auf der Konsole dargestellt werden. Wir greifen hier thematisch vor, da wir uns mit der Ein- und Ausgabe von Dateien erst im nächsten Kapitel beschäftigen werden. Wir halten es daher hier so knapp wie möglich:
try {
    BufferedReader reader = new BufferedReader(new FileReader("info.txt")); // Datei öffnen
    String s = reader.readLine(); // 1. Zeile auslesen
    System.out.println(s);
    reader.close();
} catch (Exception e) {
    e.printStackTrace();
}Für das zeilenweise Auslesen der Datei verwenden wir einen BufferedReader. Da wir damit jedoch keine Dateien öffnen können, wird dem BufferedReader gleich bei der Instanziierung ein FileReader-Objekt als Argument mitgegeben. Dieses wird automatisch mit dem BufferedReader-Objekt gekapselt und übernimmt die Aufgabe, die Datei zu öffnen.
Mit der BufferedReader-Methode readline() wird dann eine Zeile aus der Datei gelesen. Wir speichern die erste Zeile der Datei in einer String-Variable und geben sie anschließend auf der Konsole aus. Am Ende schließen wir mit reader.close() die Datei wieder.
Für gewöhnlich wird beim Auslesen einer Datei nicht nur die erste Zeile benötigt, sondern der gesamte Inhalt. Um den Code aber einfach zu halten, verzichten wir hier darauf.
In diesem Programm kann an mehreren Stellen etwas schiefgehen, das nicht in unserer Hand liegt:
| Ausnahmesituation | Auslöser-Methode | Exception-Typ | 
|---|---|---|
| Datei kann nicht geöffnet werden | FileReader(String fileName) | FileNotFoundException | 
| Fehler beim Lesen der Datei | readLine() | IOException | 
| Datei kann nicht geschlossen werden | close() | IOException | 
Wir rufen also drei riskante Methoden auf, die in einer Ausnahmesituation (d.h. bei Fehlschlag) jeweils eine checked Exception werfen. Deshalb muss jede von ihnen mit einem try-catch-Block "behandelt" werden. Der Compiler fordert das Exception Handling beim Einsatz dieser Methoden strikt ein; fehlt es, ist der Code nicht compilierbar.
Anhand der Methodendeklaration der Java-API können wir ablesen, ob eine Methode eine checked Exception werfen kann - und falls ja: um welchen Exception-Typ es sich handelt. Am Beispiel der Methode readLine() sieht das so aus:
Neu ist das Schlüsselwort throws ("wirft"), das anzeigt, dass die Methode eine checked Exception werfen kann. Dahinter steht die Klasse des Exception-Typs.
Halten wir also fest:
Bisher haben wir riskante Methoden immer direkt mit einem try-catch-Block umgeben. Exceptions müssen aber nicht "an Ort und Stelle" behandelt werden. Alternativ können wir die Exception auch "weiterwerfen".
public class Falcon {
public void dateiLesen() throws Exception{
        BufferedReader reader = new BufferedReader(new FileReader("info.txt")); // riskant!
        String s = reader.readLine(); // riskant!
        System.out.println(s);
        reader.close(); // riskant!
    }
    public static void main(String[] args){
        // Exception Handling:
        try {
            new Falcon().dateiLesen();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}Wie wir sehen, behandelt die Methode dateiLesen() den riskanten Code nicht (es gibt kein try-catch-Statement). Was aber auffällt, ist, dass die Methodendeklaration von dateiLesen() um throws Exception erweitert wurde. Das bedeutet, dass die Exception an diejenige Methode weitergeworfen wird, die dateiLesen() aufruft. In unserem Beispiel ist das die Main-Methode - und genau hier findet das Exception Handling dann auch statt.
Wir können die Behandlung von checked Exceptions auf andere Methoden auf dem Stack auslagern, aber auslassen können wir sie nicht. Der Compiler fordert für den riskanten Code unbedingt ein Exception-Handling. Entweder direkt oder über das Weiterwerfen via throws-Deklaration.
Java Basics
[Java einrichten] [Variablen] [Primitive Datentypen] [Operatoren] [if else] [switch-case] [Arrays] [Schleifen]
Objektorientierung
[Einstieg] [Variablen ] [Konstruktor] [Methoden] [Rekursion] [Statische Member] [Initializer] [Pass-by-value] [Objektsammlungen] [Objektinteraktion] [Objekte löschen]
Klassenbibliothek
[Allgemeines] [String ] [Math] [Wrapper] [Scanner] [java.util.Arrays] [Date-Time-API]
Vererbung
[Einstieg Vererbung] [Konstruktoren bei Vererbung ] [Der protected Zugriffsmodifikator] [Abstrakte Klassen und Methoden] [Polymorphie in Java] [Typumwandlung] [Die Klasse Object] [Die toString()-Methode] [Objekte vergleichen] [Was ist ein Interface?]