Arten von Ausnahmen
Kategorien von Ausnahmen
- Errors - Schwerwiegende Fehler, von denen man sich im Allgemeinen nicht erholen kann
- Speicher geht aus
- VM hat andere Probleme
- (Checked) Exceptions – Fehler, die behandelt werden sollten
- Datei nicht gefunden
- Falsche IP-Adresse angegeben
- Falsches SQL-Statement
- Runtime Exceptions – Programmierfehler, die man im Allgemeinen nicht behandelt
- Zugriff auf
null
- Division durch 0
Es gibt in Java drei grundlegende Arten von Ausnahmen. Sie werden zwar alle mit denselben Mittel erzeugt und behandelt, treten aber in unterschiedlichen Situationen auf.
Die JavaDoc der drei Arten von Ausnahmen lautet:
- Error - An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a „normal“ condition, is also a subclass of Error because most applications should not try to catch it. A method is not required to declare in its throws clause any subclasses of Error that might be thrown during the execution of the method but not caught, since these errors are abnormal conditions that should never occur.
- Exception - The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.
- RuntimeException - RuntimeException is the superclass of those exceptions that can be thrown during the normal operation of the Java Virtual Machine. A method is not required to declare in its throws clause any subclasses of RuntimeException that might be thrown during the execution of the method but not caught.
- Throwable - The Throwable class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw statement. Similarly, only this class or one of its subclasses can be the argument type in a catch clause. Instances of two subclasses, java.lang.Error and java.lang.Exception, are conventionally used to indicate that exceptional situations have occurred. Typically, these instances are freshly created in the context of the exceptional situation so as to include relevant information (such as stack trace data).
Hierarchie der Java-Ausnahmen
Errors sind hier grün, Checked Exceptions blau und Runtime Exceptions braun dargestellt.
Die Klasse Throwable
public class Throwable implements Serializable {
public Throwable() { ... }
public Throwable(String message) { ... }
public Throwable(String message, Throwable cause) { ... }
public Throwable(Throwable cause) { ... }
public String getMessage() { ... }
public String getLocalizedMessage() { ... }
public Throwable getCause() { ... }
public void printStackTrace() { ... }
public void printStackTrace(PrintStream s) { ... }
public void printStackTrace(PrintWriter s) { ... }
public synchronized native Throwable fillInStackTrace();
public StackTraceElement[] getStackTrace() { ... }
}
Die Klasse Throwable
ist die Basisklasse aller Ausnahmen in Java. Sie vererbt die wichtigsten Methoden an die von ihr abgeleiteten Ausnahmen.
Die Bedeutung der Methoden ist:
public Throwable(String message)
: Anlegen einer neuen Ausnahme mit einer für Menschen lesbaren Nachricht. Die Nachricht wird normalerweise nicht vom Programm ausgewertet (dem reicht der Typ der Ausnahme), sondern ist für menschliche Betrachter gedacht, die einen Fehler analysieren möchten.public Throwable(Throwable cause)
: Zusätzlich zu der Nachricht kann man der Ausnahme noch einen Ursache (cause
) mitgeben. Hier kann man eine weitere Ausnahme anhängen, die die Ursache für diese Ausnahme ist.public String getMessage()
: Die Nachricht kann mit dieser Methode wieder ausgelesen werden.public String getLocalizedMessage()
: WiegetMessage()
nur, dass die Nachricht in eine andere Sprache übersetzt werden kann.public Throwable getCause()
: Liefert die Ursache, die im Konstruktor übergeben wurde, wieder zurück.public void printStackTrace()
: Druckt die Aufrufhierarchie aus, die in dem Moment bestand, als die Ausnahme geworfen (nicht gefangen) wurde.public StackTraceElement[] getStackTrace()
: Liefert die Aufrufhierarchie als Array zurück, das man programmatisch untersuchen kann.
Die Klasse Exception
package java.lang;
public class Exception extends Throwable {
public Exception() { ... }
public Exception(String message) { ... }
public Exception(String message, Throwable cause) { ... }
public Exception(Throwable cause) { ... }
}
Die Klasse Exception
fügt keine neuen Methoden hinzu, sondern enthält nur Konstruktoren, da Konstruktoren in Java nicht vererbt werden.
Beispiele für häufige Ausnahmen
ArithmeticException
NullPointerException
NegativeArraySizeException
ArrayIndexOutOfBoundsException
SecurityException
IllegalStateException
- …
Welche Bedeutung diese Exceptions haben kann der Leser in der entsprechenden JavaDoc selbst nachschlagen.
Die handle or declare Regel
Für Ausnahmen gilt die handle or declare Regel
- Die Ausnahme wird durch einen try/catch behandelt oder
- Die Ausnahme muss bei der Methode mit dem
throws
Schlüsselwort angegeben werden
Gilt nicht für Runtime Exceptions und Error
- Programmierfehler sollen nicht behandelt werden
- Error können (eigentlich) nicht behandelt werden
public void storeToFile(String fileName, ResultSet rs)
throws FileNotFoundException, IOException {
...
}
Eines der Ziele beim Entwurf der Ausnahmebehandlung für Java war es, dass der Compiler erkennen kann, ob die möglichen Fehler alle behandelt werden. Um dies zu gewährleisten, gibt es die sog. handle or declare Regel: Eine Methode muss die Ausnahme entweder mit catch
(handle) fangen oder sie muss in ihrem Methodenkopf angeben, dass sie eben dies nicht tut (declare). Für letzteres dient ein neues Schlüsselwort throws
, das nach der Parameterliste der Methode angegeben wird und alle potenziell von der Methode geworfenen Ausnahmen angibt.
Diese Regel bezieht sich aber nur auf die sogenannten Checked-Exceptions also alle Ausnahmen, die keine Runtime-Exceptions und keine Errors sind. Die Motivation für diese Beschränkung ist, dass Runtime-Exceptions als Programmierfehler viel zu häufig sind und damit in nahezu jedem Java-Statement auftreten können (man denke an die NullpointerException
). Außerdem ist es sinnvoll, Programmierfehler zu erkennen und nicht durch Fangen zu verschlucken. Im Gegensatz dazu sind Errors so schwerwiegend, dass ein Fangen nichts nützen würde, da man sie nicht sinnvoll behandeln kann. So gibt es z. B. keine Möglichkeit für ein Java-Programm nach einem OurOfMemoryError
noch sinnvoll weiterzulaufen.
Beispiel: Handle or Declare
public void openFile(String dateiName) throws IOException, FileNotFoundException {
// Datei öffnen
}
public void datenSchreiben(String dateiName, String sqlStatement)
throws FileNotFoundException, IOException, SQLException {
openFile(dateiName);
// Mit Datenbank arbeiten
}
public void dateiAnlegen(String dateiName) throws FileNotFoundException, IOException {
try {
datenSchreiben(dateiName, "SELECT * FROM x");
} catch (SQLException ex) {
// Datenbank Problem beheben ;-)
}
}
public void userInterface() {
String dateiName = askUser();
try {
dateiAnlegen(dateiName);
}
catch (FileNotFoundException ex) {
// Benutzer:in erneut nach Dateinamen Fragen
}
catch (IOException ex) {
// Benutzer:in auf Problem hinweisen
}
}
Das Beispiel zeigt den Umgang mit der Handle-or-Declare-Regel: die Methode openFile
kann sowohl an einer fehlenden Datei (FileNotFoundException
) als auch generellen Problemen beim Dateizugriff scheitern (IOException
). Deswegen deklariert die Methode beide Exceptions über throws
.
Die Methode datenSchreiben
verwendet die openFile
-Methode. Damit können potenziell ebenfalls die beiden Ausnahmen FileNotFoundException
und IOException
entstehen. Da sie keine von beiden fängt, muss sie beide weiter deklarieren. Zusätzlich führt Sie noch einen Datenbankzugriff durch, der einen Fehler durch eine SQLException
anzeigen könnte (der Datenbankzugriff ist hier nur durch den Kommentar angedeutet.)
Die Methode dateiAnlegen
behandelt die SQLException
, nicht jedoch die IOException
oder die FileNotFoundException
, weswegen sie beide per throws
deklarieren muss.
Die letzte Methode userInterface
fragt den Dateinamen vom Benutzer ab und behandelt alle noch vorhandenen Ausnahmen selbst. Deshalb muss sie keinen Ausnahme per throws
deklarieren.
Ausnahmen und Überschreiben
Überschriebene Methoden können
- Deklarierte Ausnahmen unverändert lassen oder
- Deklarierte Ausnahmen weglassen oder
- Deklarierte Ausnahmen durch ihre Subklassen ersetzen oder
- Ausnahmen hinzufügen, die Subklassen der deklarierten sind
Sie können auf keinen Fall neue Checked Exceptions hinzufügen
Grund: Polymorphie
In einem vorherigen Kapitel wurde bereits diskutiert, dass überschriebene Methoden die Sichtbarkeit und den Rückgabetyp nicht ändern dürfen. Im Zusammenhang mit Ausnahmen muss diese Regel noch erweitert werden: Überschriebene Methoden dürfen mit throws
deklarierte Ausnahmen unverändert lassen, Ausnahmen weglassen oder durch Subklassen ersetzen.
Der Hintergrund liegt in der Polymorphie begründet. Der statische Typ der Referenzvariable legt fest, welche Methoden über die Variable aufgerufen werden können. Damit wird aber über den statischen Typ bestimmt, welche Ausnahmen potenziell aus einem Methodenaufrufe entstehen können. Da der Verwender einer Variable nur deren statischen Typ kennt, wird er sich bei der Fehlerbehandlung an eben diesem orientieren. Würde man jetzt gestatten, dass eine überschriebene Methode in einer Subklasse neue Ausnahmen werfen dürfte, hätte der Verwender keine Chance sich darauf einzurichten. Daher wird dies vom Compiler unterbunden. Anders ausgedrückt kann man sagen, dass die catch
-Blöcke, die für einen Methodenaufruf auf einem Typ geschrieben wurden, genauso für alle Subtypen passen müssen.
Beispiel: Überschreiben und Exceptions
class A {
public void method() throws IOException { ... }
}
class B extends A {
public void method() throws IOException, FileNotFoundException { ... }
}
class C extends A {
public void method() throws FileNotFoundException { ... }
}
class D extends A {
public void method() { ... }
}
class E extends A {
public void method() throws SQLException { ... } // FEHLER!!
}
Nach den gerade erläuterten Regel ist die überschriebene Methode method
in der Klasse E
unzulässig, weil sie mehr Ausnahmen wirft, als die gleichnamige Methode in A
. Ein Verwender würde überrascht, denn er würde nur catch
-Blöcke für IOException
vorsehen.
Die Klassen B
–D
sind zulässig:
B
: DaFileNotFoundException
eine Subklasse vonIOException
ist, ist diese zusätzliche Ausnahme zulässig, da eincatch
-Block fürIOException
sie auch fangen würde.C
:FileNotFoundException
ist eine Subklasse vonIOException
und würde daher auch von vorhandenencatch
-Blöcken erfasst.D
: Über das Weglassen einer Ausnahme wird kein Verwender böse sein. Der vorhandenecatch
-Block wird nicht genutzt, es entsteht aber kein Schaden.
Ausnahmen und Programmierstil
Folgende Konstruktionen sollten vermieden werden
- Sehr lange
try
-Blöcke – Dertry
-Block sollte nur den Code umfassen, der den Fehler erzeugen kann - Catchen von Exception – Da
Exception
die Mutter aller Ausnahmen ist fängt man damit auch Runtime-Exceptions, die man aber nicht fangen sollte - Leere
catch
-Blöcke – Eine Ausnahme signalisiert einen Fehlerfall, sie einfach zu ignorieren ist fast nie nichtig - printStackTrace() –
catch
-Blöcke, die die Ausnahme nur ausdrucken, ignorieren ebenfalls den Fehler statt ihn zu behandeln
Diese Regeln gelten auch für die Pflichtübungen. Verletzungen führen grundsätzlich zu Punktabzug.
Beispiel: Schlechter Stil
try {
FileInputStream fis = new FileInputStream("/tmp/gibtsnicht");
String s = null;
s = s.toUpperCase();
} catch (Exception e) { // SCHLECHT!!
// Fehlerbehandlung
}
FileInputStream fis = null;
try {
fis = new FileInputStream("/tmp/gibtsnicht");
} catch (IOException e) {
e.printStackTrace(); // SCHLECHT!!
}
fis.read();
Das Beispiel zeigt einen schlechten Stil im Umgang mit Ausnahmen, der zu echten Problemen führen wird.
Das Fangen von Exception
im ersten catch
-Block führt dazu, dass nicht nur eine FileNotFoundException
aus dem Dateizugriff gefangen wird, sondern alle Runtime-Exceptions. Hierdurch wird der Programmierfehler, der sich noch im ersten try
-Block befindet, verschleiert. Der/die Programmierer:in merkt gar nicht, dass eine NullPointerException
auftritt, da diese in der Fehlerbehandlung für den Dateizugriff untergeht. Umgekehrt kann es auch passieren, dass man einen Fehler bei der Datei sucht obwohl diese ganz normal gelesen werden könnte und der catch
-Block nur wegen der null
-Referenz angesprungen wurde.
Im zweiten catch
-Block wird zwar die richtige Ausnahme gefangen, diese wird aber einfach auf der Konsole ausgedruckt, anstatt sie zu behandeln. Da echte Programme oft unbeaufsichtigt laufen, ist die Verwendung der Konsole grundsätzlich eine schlechte Idee.