Try-Catch
Beispiel: Ausnahme
public class ExceptionExample {
public static void main(String[] args) {
String[] namen = { "Franz", "Hans", "Alfons" };
for (int i = 0; i < 4; i++) {
System.out.print(namen[i] + ", ");
}
}
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at pr2.ExceptionExample.main(ExceptionExample.java:10)
Franz, Hans, Alfons,
Das Beispiel zeigt, was passiert, wenn in Java eine Ausnahme auftritt. Das Programm versucht hinter das letzte Element des Arrays zu greifen, was von der Laufzeit erkannt wird und zum Auslösen einer ArrayIndexOutOfBoundsException
führt.
Man sieht an dem Beispiel schon sehr schön die Vorteile der Fehlerbehandlung in Java: Der Typ der Ausnahme weist direkt durch seinen Namen darauf hin, welchen Fehler man gemacht hat. Zusätzlich enthält die Ausnahme die Information, welchen ungültigen Array-Index man verwendet hat (3). Der Call-Stack zeigt die Datei und die Zeilennummer an, in der der Fehler aufgetreten ist.
Fehlerbehandlung in Java
- Fehler werden durch eine Ausnahmen (exceptions) signalisiert
- Exceptions sind Java Objekte, die im Fehlerfall erzeugt werden
- Alle Exceptions sind (indirekt) von
Throwable
abgeleitet - try-Block enthält Code, der mit Fehlerbehandlungsroutine(n) versehen werden soll
- Tritt ein Fehler auf, wird die Fehlerbehandlungsroutine angesprungen (
try
-Block wird abrupt beendet) - Tritt kein Fehler auf, läuft das Programm nach dem try/catch weiter (
try
-Block wird normal beendet) - catch-Blöcke enthalten die Fehlerbehandlungsroutine (exception handler) zum
try
-Block - Fehler wird als Objekt an
catch
übergeben
Das Exception-Konzept von Java ist stark an die C++-Exceptions angelehnt.
Jeder Fehler (Exception) wird durch ein Objekt repräsentiert, dessen Klasse von Throwable
abgeleitet ist. Der Typ des Objektes liefert eine genaue Information, welche Art von Fehler aufgetreten ist.
Die Fehlerbehandlung wird von der normalen Programmlogik dadurch getrennt, dass man zwei unterschiedliche Blöcke verwendet. Der eine Block wird mit try
eingeleitet und enthält die Anweisungen, die möglicherweise zu einem Fehler führen könnten, z. B. Zugriff auf eine Datei. Der andere Block wird mit catch
gekennzeichnet und enthält die Fehlerbehandlung. Er wird nur dann angesprungen, wenn im try
-Block ein Fehler aufgetreten ist. Wenn ein solcher Fehler auftritt, wird der try
-Block abrupt beendet, d. h. er wird nicht weiter ausgeführt, sondern die Programmausführung springt zu dem catch
-Block. Man sagt auch, dass der catch
-Block die Ausnahme fängt (Fangen einer Ausnahme). Wenn kein Fehler auftritt, läuft der try
-Block ganz normal zu Ende und der catch
-Block wird ignoriert. D. h. die Programmausführung geht nach dem catch
-Block weiter.
Über einen Parameter wird dem catch
-Block das Ausnahmeobjekt übergeben, auf das er dann über eine lokale Referenzvariable zugreifen kann. Nachdem der catch
-Block am Ende angekommen ist, wird die Programmausführung nach dem catch
-Block fortgesetzt.
Beispiel: try und catch
Connection connection = getConnection();
try {
ResultSet rs = readData(connection);
while (rs.next()) {
// Daten bearbeiten
}
} catch (SQLException ex) {
// Datenbank Problem behandeln
String sqlStatus = ex.getSQLState();
ex.printStackTrace();
...
}
...
Dieses Beispiel zeigt, wie eine potenziell auftretende SQLException
behandelt werden kann – hierbei handelt es sich um einen Fehler, der bei der Kommunikation mit einer Datenbank auftreten kann. Im try
-Block befindet sich der Datenbankzugriff, der sich hinter der Methode readData()
und den Zugriffen auf das ResultSet
mit rs.next()
verbirgt. Bei jedem Datenbankzugriff kann potenziell ein Problem entstehen, z. B. ist die Datenbank nicht verfügbar, das dann durch eine SQLException
signalisiert wird.
Die Behandlung des Datenbankfehlers erfolgt in diesem Beispiel im entsprechenden catch
-Block, der die SQLException
fängt. Obwohl die eigentliche Fehlerbehandlung hier weggelassen wurde, kann man eine weitere wichtige Eigenschaft von Exceptions erkennen: Dadurch, dass sie echte Java-Objekte sind, kann man über die Ausnahme zusätzliche Daten transportieren. Hier den Fehler-Code der Datenbank, der mit getSQLState()
ausgelesen wird und den Stack-Trace, der mit printStackTrace()
ausgedruckt wird.
Die hier dargestellte Fehlerbehandlung ist nur als Beispiel zu verstehen und natürlich nicht realistisch. Vor allem das Ausdrucken des Stack-Traces ergibt in echten Systemen nur wenig Sinn.
Mehrere catch-Blöcke
- Man kann beliebig viele
catch
-Blöcke angeben - Die Laufzeit wählt den Block aus, bei dem die angegebene Ausnahme am besten zur tatsächlich geworfenen passt
- Die Auswahl geschieht anhand der Vererbungshierarchie der Ausnahmen
- Die Blöcke müssen in der Reihenfolge vom Spezifischen zum Allgemeinen angegeben werden
In einem try
-Block können durchaus verschiedene Ausnahmen auftreten, die man unterschiedlich behandeln möchte. Daher ist es möglich, einem try
- mehrere catch
-Blöcke zuzuordnen.
Die Auswahl des catch
-Blockes erfolgt dann zur Laufzeit anhand des Typs der Ausnahme, die im try
-Block geworfen wurde. Ähnlich zu überladenen Methoden wird also der am besten passenden Block ausgewählt. (Anders als bei überladenen Methoden aber erst zur Laufzeit und nicht bereits zur Compilezeit).
Da die Laufzeit die catch
-Blöcke bei der Suche nach dem richtigen Block strikt von oben nach unten durchgeht, ist es wichtig die Blöcke vom Spezifischen zum Allgemeinen hin anzuordnen. D. h. man muss zuerst den catch
-Block mit der Ausnahme schreiben, die in der Vererbungshierarchie am weitesten unten steht.
Beispiel: Mehrere catch-Blöcke
Connection connection = getConnection();
try {
ResultSet rs = readData(connection);
while (rs.next()) {
storeToFile(rs.getString(0), rs);
}
} catch (SQLException ex) {
// Datenbank-Problem
ex.printStackTrace();
} catch (FileNotFoundException ex) {
// Datei nicht gefunden
System.out.println(ex.getMessage());
} catch (IOException ex) {
// Allgemeiner IO-Fehler
System.out.println(ex.getMessage());
}
Man erkennt im Beispiel deutlich die Regel, dass die catch
-Blöcke vom Speziellen zum Allgemeinen hin angeordnet werden müssen. SQLException
und IOException
stehen in keiner Beziehung zueinander, sodass hier die Anordnung egal ist. Da FileNotFoundException
spezifischer als IOException
ist (siehe Abbildung), muss ihr catch
-Block jedoch zuerst aufgeführt werden.
Multi-Catch
- Für einen
catch
-Block können beliebig viele Ausnahmen (durch | getrennt) angegeben werden - Die Ausnahmen dürfen nicht Subklassen voneinander sein
- Jeder Ausnahme-Typ darf nur einmal genannt werden
try {
openFile();
writeToDatabase();
}
catch (FileNotFoundException | SQLException e) {
System.out.println("Ohje, alles kaputt...:" + e);
}
Es kommt häufig vor, dass eine Methode viele verschiedene Ausnahmen werfen kann. Dies führt beim Verwender zu einer ganzen Reihe von catch
-Blöcken, die oft alle dasselbe tun. Eine Möglichkeit wäre natürlich, anstelle vieler catch
-Blöcke nur einen catch
-Block der Superklasse Exception
zu verwenden. Dies gilt aber als sehr schlechter Stil (siehe oben) und ist daher keine befriedigende Lösung.
Seit Java 7 besteht die Möglichkeit, mehrere Ausnahmen in einem einzigen catch
-Block zu fangen. Die Ausnahmen werden durch |
getrennt und dürfen in keiner Vererbungsbeziehung zueinander stehen.
Der statische Typ der Referenz auf die Ausnahme (e
im Beispiel) in einem multi-catch ist dann allerdings Exception
. Will man auf spezielle Attribute der Ausnahmen zugreifen, muss man einen Cast auf ihren dynamischen Typ durchführen.
So nicht!
public static void printArray(int[] array) {
try {
for (int i = 0;;i++) {
System.out.println(array[i]);
}
}
catch (ArrayIndexOutOfBoundsException ex) {
}
}
Der/die Programmierer:in dieses Beispiels hat versucht, die Prüfung auf die Grenzen des Arrays zu sparen und lieber die ArrayIndexOutOfBoundsException
abzufangen, da er/sie glaubt, dass das einmalige Fangen einer Ausnahme schneller geht als eine Prüfung bei jedem Schleifendurchlauf. Obwohl dieses Programm semantisch korrekt funktioniert, handelt es sich um einen Missbrauch von Ausnahmen, der zur Folge hat, dass das Programm viel langsamer läuft. Der Grund liegt darin, dass die Java-VM normalerweise die Schleifengrenzen nur einmal überprüft und dann sehr effizienten Maschinencode generieren kann. Dies ist hier nicht möglich, da es keine Grenzen im Programm gibt. Deshalb muss sie bei jedem Durchlauf erneut auf die Grenzen testen und entsprechend eine Ausnahme generieren, wenn sie überschritten wurden.
Vergleich zu C
if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < O) {
DieWithError("socket () failed") ;
}
if (bind(servSock, (struct sockaddr *)&echoServAddr,
sizeof(echoServAddr)) < O) {
DieWithError ("bind () failed");
}
if (listen(servSock, MAXPENDING) < O) {
DieWithError("listen() failed") ;
}
for (;;) {
if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr,
&clntLen)) < O) {
DieWithError("accept() failed");
}
}
Hier ist noch einmal ein Beispiel der Fehlerbehandlung in der Programmiersprache C dargestellt, das im ersten Kapitel bereits gezeigt wurde.
try {
ServerSocket servSocket = new ServerSocket();
servSocket.bind(new InetSocketAddress(8080));
for (;;) {
Socket clntSocket = servSocket.accept();
// ...
}
}
catch (SocketTimeoutException e) {
// Fehlerbehandlung
}
catch (IOException e) {
// Fehlerbehandlung
}
Bei der Java-Variante des Programms kann man sehr gut die Unterschiede erkennen. Hier vermischt sich die Fehlerbehandlung nicht mehr mit dem normalen Programmablauf. Man kann deutlich sehen, wie man einen Netzwerkserver programmiert, ohne von der Fehlerbehandlung verwirrt zu werden.