7.1 Problembereiche einzäunen
Werden in C Routinen aufgerufen, dann haben diese keine andere Möglichkeit, als über den Rückgabewert einen Fehlschlag anzuzeigen. Der Fehlercode ist häufig -1, aber auch NULL oder 0. Allerdings kann die Null auch Korrektheit anzeigen. Irgendwie ist das willkürlich. Die Abfrage dieser Werte ist unschön und wird von uns gerne unterlassen, zumal wir oft davon ausgehen, dass ein Fehler in dieser Situation gar nicht auftreten kann - diese Annahme kann eine Dummheit sein. Zudem wird der Programmfluss durch Abfragen der Funktionsergebnisse unangenehm unterbrochen, zumal der Rückgabewert, wenn er nicht gerade einen Fehler anzeigt, weiterverwendet wird. Der Rückgabewert ist also im weitesten Sinne überladen, da er zwei Zustände anzeigt. Häufig entstehen mit den Fehlerabfragen kaskadierte if-Abfragen, die den Quellcode schwer lesbar machen.
7.1.1 Exceptions in Java mit try und catch
Bei der Verwendung von Exceptions wird der Programmfluss nicht durch Abfrage des Rückgabestatus unterbrochen, sondern ein besonders ausgezeichnetes Programmstück wird bezüglich auftretender Fehler überwacht und gegebenenfalls spezieller Code zur Behandlung solcher Fehler aufgerufen. Der überwachte Programmbereich (Block) wird durch das Schlüsselwort try eingeleitet und durch catch beendet. Hinter dem catch folgt der Programmblock, der beim Auftreten eines Fehlers ausgeführt wird, um den Fehler abzufangen oder zu behandeln. Daher auch der Ausdruck catch.
Hier klicken, um das Bild zu Vergrößern
7.1.2 Eine Datei auslesen mit RandomAccessFile
Wir wollen eine Datei mithilfe der Klasse RandomAccessFile zeilenweise auslesen. Die Verbindung zwischen der Datei und dem zugehörigen Objekt gelingt mit dem Konstruktor, dem wir einen Dateinamen mitgeben.
Hier klicken, um das Bild zu Vergrößern
Eine nicht behandelte Ausnahme wird von Eclipse als Fehler angezeigt.
Hier klicken, um das Bild zu Vergrößern
Aus der API-Dokumentation geht hervor, dass der Konstruktor von RandomAccessFile eine FileNotFound Exception-Ausnahme auslösen kann. Mithilfe der Funktion readLine() lesen wir so lange Zeilen ein, bis die Datei ausgeschöpft ist. Die Methode readLine() kann eine IOException auslösen. Wir müssen diese behandeln und setzen daher die Problemzonen in einen try- und catch-Block.
Listing 7.1 ReadFileWithRAF.java
import java.io.*;
public class ReadFileWithRAF
{
public static void main( String args[] )
{
try
{
RandomAccessFile f;
f = new RandomAccessFile( "c:/winnt/desktop.ini", "r" );
String line;
while ( (line = f.readLine()) != null )
System.out.println( line );
f.close();
}
catch ( FileNotFoundException e ) // Datei gibt's nich'
{
System.err.println( "Datei gibt's nicht." );
}
catch ( IOException e ) // Schreib-/ Leseprobleme
{
System.err.println( "Schreib- Leseprobleme" );
}
}
Tritt beim Laden einer Datei ein Fehler auf, wird dieser im try-Block abgefangen und im catch-Teil bearbeitet. Einem try-Block können mehrere catch-Klauseln zugeordnet sein, um verschiedene Fehlertypen aufzufangen.
Ein try/catch-Block kann Eclispe auch selbst anlegen. Dazu wird wieder (Strg)+(1) bemüht und aus Auswahl steht einmal die Möglichkeit den Fehler an den Aufrufer weiterzuleiten - das schauen wir uns etwas später an - und ein try/catch-Block anzulegen.
Hier klicken, um das Bild zu Vergrößern
Leere catch-Blöcke
Java schreibt vor, dass Ausnahmen in einem catch behandelt (oder nach oben geleitet) werden, aber nicht, was in catch-Blöcken zu geschehen hat. Ein leerer catch-Block ist in der Regel wenig sinnvoll, denn dann werden die Fehler klammheimlich unterdrückt. (Das wäre genauso wie ignorierte Statusrückgabewerte von C-Funktionen.) Das Mindeste ist eine minimale Fehlerbehandlung wie System.err.println(e) oder das informativere e.printStackTrace() für eine Exception e.
7.1.3 Ablauf einer Ausnahmesituation
Ein Ausnahme-Objekt wird vom Laufzeitsystem erzeugt, wenn ein Fehler über eine Exception angezeigt werden soll. Dann wird die Abarbeitung der Programmzeilen sofort unterbrochen, und das Laufzeitsystem steuert selbstständig die erste catch-Klausel an (oder springt weiter zum Aufrufer, wie wir später sehen werden). Wenn die erste catch-Anweisung nicht zur Art des aufgetretenen Fehlers passt, werden der Reihe nach alle übrigen catch-Klauseln untersucht und die erste übereinstimmende Klausel wird angesprungen (oder ausgewählt). Erst wird etwas versucht, und wenn im Fehlerfall ein Exception-Objekt in den Programmtext geworfen wird, kann es an einer Stelle aufgefangen werden. Da immer die erste passende catch-Klausel ausgewählt wird, darf im Beispiel die letzte catch-Klausel keinesfalls zuerst stehen, da diese auf jeden Fehler passt. Alle anderen Anweisungen in den catch-Blöcken würden dann nicht ausgeführt. Mittlerweile erkennt der Compiler dieses Problem und gibt einen Fehler aus.
7.1.4 Wiederholung kritischer Bereiche
Es gibt in Java bei Ausnahmen bisher keine von der Sprache unterstützte Möglichkeit, an den Punkt zurückzukehren, der den Fehler ausgelöst hat. Das ist aber oft gewünscht, etwa in dem Fall, wenn eine fehlerhafte Eingabe zu wiederholen ist.
Wir werden mit JOptionPane.showInputDialog() nach einem String fragen und versuchen, diesen in eine Zahl zu konvertieren. Dabei kann natürlich etwas schief gehen. Wenn ein Benutzer eine Zeichenkette eingibt, die keine Zahl repräsentiert, dann wird eine NumberFormatException ausgelöst. Wir wollen in diesem Fall die Eingabe wiederholen.
Listing 7.2 ContinueInput.java
import javax.swing.*;
public class ContinueInput
{
public static void main( String args[] )
{
int number = 0;
while ( true )
{
try
{
String s = JOptionPane.showInputDialog(
"Bitte Zahl eingeben" );
number = Integer.parseInt( s );
break;
}
catch ( NumberFormatException e )
{
System.out.println( "Das war keine Zahl!" );
}
}
System.out.println( "Danke für die Zahl " + number );
System.exit( 0 ); // Beendet die Anwendung
}
}
Die gewählte Lösung ist einfach. Wir programmieren den gesamten Teil in einer Endlosschleife. Geht die problematische Stelle ohne Fehler durch, so beenden wir die Schleife mit break. Kommt es zu einer Ausnahme, dann wird break nicht ausgeführt, und nach der Exception gelangen wir wieder in die Endlosschleife.
7.1.5 throws im Methodenkopf angeben
Neben dem Einzäunen von problematischen Blöcken durch einen try- und catch-Block gibt es noch eine andere Möglichkeit, auf Exceptions zu reagieren: Im Kopf der betreffenden Methode wird eine throws-Klausel eingeführt. Dadurch zeigt die Methode an, dass sie eine bestimmte Exception nicht selbst behandelt, sondern diese unter Umständen an die aufrufende Methode weitergibt. Nun kann von einer Funktion eine Exception ausgelöst werden. Die Funktion wird abgebrochen und gibt ihrerseits eine Exception zurück.
Beispiel Eine Methode soll eine Datei öffnen und die erste Zeile auslesen. Der Dateiname wird als Parameter der Methode übergeben. Da das Öffnen der Datei sowie das Lesen einer Zeile eine Ausnahme auswerfen kann, müssen wir diese Ausnahme behandeln. Wir fangen sie jedoch nicht in einem eigenen try- und catch-Block auf, sondern leiten sie an den Aufrufer weiter. Das bedeutet, dass er sich um den Fehler kümmern muss.
String readFirstLineFromFile( String filename )
throws FileNotFoundException, IOException
{
RandomAccessFile f = new RandomAccessFile( filename, "r" );
return f.readLine();
}
|
Dadurch »bubbelt« der Fehler entlang der Kette von Methodenaufrufen nach oben und kann irgendwann von einem Block abgefangen werden, der sich darum kümmert.
Wenn main() alles weiterleitet
Ist die Fehlerbehandlung in einem Hauptprogramm ganz egal, so können wir alle Fehler auch an die Laufzeitumgebung weiterleiten, die dann das Programm im Fehlerfall abbricht.
Listing 7.3 MirIstAllesEgal.java
import java.io.*;
class MirIstAllesEgal
{
public static void main( String args[] ) throws Exception
{
RandomAccessFile f = new RandomAccessFile( "Datei.txt", "r" );
System.out.println( f.readLine() );
}
}
Das funktioniert, da alle Fehler von der Klasse Exception1 abgeleitet sind. Wir werden das in den folgenden Kapiteln weiterverfolgen. Wird der Fehler nirgendwo sonst aufgefangen, dann wird eine Laufzeitfehlermeldung ausgegeben, denn das Exception-Objekt ist beim Interpreter, also bei der virtuellen Maschine, auf der äußersten Aufrufebene gelandet. Natürlich ist das kein guter Stil - obwohl es aus Gründen kürzerer Programme auch in diesem Buch so gemacht wird. Denn Fehler sollten in jedem Fall behandelt werden.
7.1.6 Abschließende Arbeiten mit finally
Nach einem catch-Block kann optional noch ein finally-Block folgen. Der Teil im finally wird immer ausgeführt, auch wenn in try und catch ein return, break oder continue steht. Das heißt, der Block wird auf jeden Fall ausgeführt. Eine typische Anwendung ist die Freigabe von Ressourcen oder das Schließen von Dateien.
Ein try ohne catch
Es kommt zu einer merkwürdigen Konstellation, wenn mit throws eine Exception nach oben geleitet wird. Dann ist ein catch für diese Fehlerart nicht notwendig. Dennoch lässt sich dann ein Block mit einer Ereignisbehandlung umrahmen, um ein finally auszuführen:
void read() throws MyException
{
try
{
// hier etwas Arbeiten, was eine MyException auslösen könnte
return;
}
finally
{
System.out.prinln( "Ja, das kommt danach" );
}
}
Ein return im finally lässt Ausnahmen verschwinden
Ein Phänomen in der Ausnahmebehandlung von Java ist eine return-Anweisung innerhalb eines finally-Blocks. Wird dort ein return eingesetzt, wird eine ausgelöste Ausnahme nicht zum Aufrufer weitergeleitet.
Beispiel Die Methode buh() löst eine ArithmeticException aus, eine spezielle Art von RuntimeException.
|
Der Aufrufer von buh() ist die main()-Funktion. Es ist zu erwarten, dass main() abbricht, denn die Exception wird doch nicht abgefangen. Dem ist aber bei einem return im finally nicht so. Erst wenn wir diese Zeile entfernen, wird die erwartete Ausnahme die Laufzeitumgebung beenden.
Listing 7.4 NoExceptionBecauseOfFinallyReturn.java
public class NoExceptionBecauseOfFinallyReturn
{
static void buh()
{
try
{
throw new ArithmeticException( "Keine Lust zu rechnen" );
}
finally
{
// Das return bewirkt normalen Rücksprung aus buh() ohne Exception
return; // Spannende Zeile.
}
}
public static void main( String args[] )
{
buh();
}
}
Hinweis Haarspalterisch genau lässt sich auch ein Beispiel finden, in dem finally nicht ausgeführt wird.
try
{
System.exit( 1 );
}
finally
{
System.out.println( "Das wirst du nicht erleben!" );
}
|
7.1.7 Nicht erreichbare catch-Klauseln
Eine catch-Klausel heißt erreichbar, wenn es in dem try- und catch-Block eine Anweisung gibt, die die Fehlerart, die in der catch-Klausel aufgefangen wird, tatsächlich auslösen kann. Zusätzlich darf vor dieser catch-Klausel natürlich kein anderes catch stehen, das diesen Fehlerfall mit abfängt. Wenn wir zum Beispiel catch(Exception e) als erstes Auffangbecken bereitstellen, dann werden natürlich alle Ausnahmen dort behandelt. Die Konsequenz daraus ist, catch-Klauseln immer von den speziellen zu den allgemeinen Fehlerarten zu sortieren.
Wenn wir ein Objekt RandomAccessFile aufbauen und anschließend readLine() verwenden, so muss eine FileNotFoundException vom Konstruktor und eine IOException von readLine() abgefangen werden. Da eine FileNotFoundException eine Spezialisierung, also eine Unterklasse von IOException ist, würde eine catch(IOException e) schon reichen, denn dies fängt alles ab, auch die FileNotFoundException. Steht im Quellcode folglich der catch für die FileNotFoundException dahinter, wird der Teil nie ausgeführt werden können und der Compiler merkt das zu Recht an.
Leere Blöcke
Da der leere Block keine Exception auslösen kann, ist der catch-Block des nächstens Programms nicht erreichbar und daher ist das Programm falsch.
Listing 7.5 NoException.java
class NoException
{
public static void main( String args[] )
{
try {
}
catch ( Exception e )
{
System.out.println( "Hab' dich" );
}
}
}
Ein Compiler sollte diese Situation erkennen, obwohl leider einige Compiler mit dieser Situation noch Schwierigkeiten haben. (Ein leerer try-Block ist natürlich auch ein recht seltener Spezialfall, sonst sind ziemlich viele Arten von Exceptions auch in scheinbar harmlosem Code denkbar: ArrayIndexOutOfBoundsException oder andere RuntimeExceptions.)
Übertriebene throws
Ein anderes Problem sind übertriebene throws-Klauseln. Es ist nicht falsch, wenn eine Methode zu viele oder zu allgemeine Fehlerarten in ihrer throws-Klausel angibt. Beim Aufruf solcher Methoden in try-Blöcken sind catch-Klauseln für die zu viel deklarierten Exceptions formal korrekt, können aber natürlich nicht wirklich erreicht werden.
1 Genauer gesagt sind alle Ausnahmen in Java von der Exception-Oberklasse Throwable abgeleitet.
|