Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 7 Exceptions
  gp 7.1 Problembereiche einzäunen
    gp 7.1.1 Exceptions in Java mit try und catch
    gp 7.1.2 Ablauf einer Ausnahmesituation
    gp 7.1.3 Wiederholung kritischer Bereiche
    gp 7.1.4 throws im Methodenkopf angeben
    gp 7.1.5 Abschließende Arbeiten mit finally
    gp 7.1.6 Nicht erreichbare catch-Klauseln
  gp 7.2 Die Klassenhierarchie der Fehler
    gp 7.2.1 Die Exception-Hierarchie
    gp 7.2.2 Ober-Ausnahmen fangen
    gp 7.2.3 Alles geht als Exception durch
    gp 7.2.4 Ausnahmen, die nicht gefangen werden müssen: RuntimeException
  gp 7.3 Werfen eigener Exceptions
    gp 7.3.1 Vorgefertigte Ausnahme-Objekte wieder verwenden
    gp 7.3.2 Typecast auf ein null-Objekt für eine NullPointerException
    gp 7.3.3 Neue Exception-Klassen definieren
  gp 7.4 Rückgabewerte bei ausgelösten Ausnahmen
  gp 7.5 Ein Assert in Java
  gp 7.6 Sicherheitsfragen mit dem SecurityManager klären
    gp 7.6.1 Programm beenden

Kapitel 7 Exceptions

»Wir sind in Sicherheit! Er kann uns nicht erreichen!«
»Sicher?«
»Ganz sicher! Bären haben Angst vor Treibsand!«
Häger, Dik Browne

Dass Fehler beim Programmieren auftauchen ist unvermeidlich. Schwierig sind nur die unkalkulierbaren Situationen und daher ist der Umgang mit Fehlern ganz besonders heikel. Java bietet die elegante Methode der Exceptions, um mit Fehlern flexibel umzugehen.


Galileo Computing

7.1 Problembereiche einzäunen  downtop

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, wogegen NULL oder 0 Korrektheit anzeigt. Die Abfrage dieser Werte ist unschön und wird von uns gerne unterlassen, zumal wir oftmals 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, weiter verwendet wird. Der Rückgabewert ist also im weitesten Sinne überladen, da er zwei Zustände anzeigt.


Galileo Computing

7.1.1 Exceptions in Java mit try und catch  downtop

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 bei Auftreten eines Fehlers ausgeführt wird, um den Fehler abzufangen oder zu behandeln. Daher auch der Ausdruck catch.

Abbildung

Beispiel Wir wollen eine Datei mit Hilfe der Klasse RandomAccessFile zeilenweise auslesen. Die Verbindung zwischen der Datei und dem zugehörigen Objekt gelingt mit dem Konstruktor, dem wir einen Dateinamen mitgeben. Mit Hilfe der Funktion readLine() lesen wir solange Zeilen ein, bis die Datei ausgeschöpft ist. Aus der API-Dokumentation geht hervor, dass der Konstruktor von RandomAccessFile eine FileNotFound Exception Ausnahme auslösen kann und die Methode readLine() eine IOException. Wir müssen diese behandeln und setzen daher die Problemzonen in einen try- und catch-Block.
 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" );
}
catch ( Exception e ) // alles andere
{
System.err.println( "Noch ein anderer Fehler.");
}
}

Listing 7.1   ReadFileWithRAF.java
import java.io.*;

public class ReadFileWithRAF
{
public static void main( String args[] )
{
try
{
RandomAccessFile f;
}

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. Das finally wird nach der Ausnahmebehandlung aufgerufen.

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.


Galileo Computing

7.1.2 Ablauf einer Ausnahmesituation  downtop

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 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 daher 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.


Galileo Computing

7.1.3 Wiederholung kritischer Bereiche  downtop

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 dem 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 );
}
}

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.


Galileo Computing

7.1.4 throws im Methodenkopf angeben  downtop

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 Dateinamen wird als Parameter der Methode übergeben. Da das Öffnen der Datei sowie das Lesen einer Zeile einer 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.

Ist die Fehlerbehandlung in einem Hauptprogramm ganz egal, so können wir alle Fehler auch einfach 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 Exception abgeleitet sind. Wir werden das in den folgenden Kapiteln weiter verfolgen. 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 Aufruf-Ebene gelandet.


Galileo Computing

7.1.5 Abschließende Arbeiten mit finally  downtop

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 ein 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();
}
}

Galileo Computing

7.1.6 Nicht erreichbare catch-Klauseln  downtop

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.

Listing 7.5   NoException.java
class NoException
{
public static void main( String args[] )
{
try {
}
catch ( Exception e ) {
System.out.println( "Hab' dich" );
}
}
}

Da der leere Block keine Exception auslösen kann, ist der catch-Block dieses Programms nicht erreichbar und daher ist das Programm falsch. Ein Compiler sollte diese Situation erkennen, obwohl leider noch einige Compiler mit dieser Situation 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.)

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.


Galileo Computing

7.2 Die Klassenhierarchie der Fehler  downtop

Eine Exception ist ein Objekt, welches direkt oder indirekt von java.lang.Throwable abgeleitet ist. Von dort aus verzweigt sich die Hierarchie der Fehlerarten nach java.lang.Exception und java.lang.Error. Die Klassen, die aus Error hervorgehen, sollen nicht weiter verfolgt werden. Es handelt sich hierbei um Fehler, die so schwer wiegend sind, dass sie zur Beendigung des Programms führen und vom Programmierer nicht weiter beachtet werden müssen und sollen. Von Throwable werden eine Reihe von nützlichen Methoden vererbt, die in der folgenden Grafik sichtbar sind. Sie fasst gleichzeitig die Vererbungsbeziehungen noch einmal zusammen.

Abbildung


Galileo Computing

7.2.1 Die Exception-Hierarchie  downtop

Jede Benutzerausnahme wird von java.lang.Exception abgeleitet. Die Exceptions sind Fehler oder Ausnahmesituationen, die vom Programmierer behandelt werden sollen. Die Klasse Exceptions teilt sich dann nochmals in weitere Unterklassen bzw. Unterhierarchien auf. Die folgende Grafik zeigt einige Unterklassen der Klasse Exception.

Abbildung


Galileo Computing

7.2.2 Ober-Ausnahmen fangen  downtop

Eine Konsequenz der Hierarchien ist, dass es ausreicht, einen Fehler der Oberklasse aufzufangen. Wenn zum Beispiel eine FileNotFoundException auftritt, dann ist diese Klasse von IOException abgeleitet, was heißt, FileNotFoundException ist eine Spezialisierung. Wenn wir alle IOException auffangen, behandeln wir damit auch gleichzeitig die FileNotFoundException mit.

Erinnern wir uns noch einmal an das Datei-Beispiel. Dort haben wir ein FileNotFoundException und eine IOException einzeln behandelt. Das lässt sich wie folgt zusammenfassen:

Listing 7.6   ReadFileWithRAFShort.java
import java.io.*;

public class ReadFileWithRAFShort
{
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 );
}
catch ( IOException e )
{
System.err.println( "Ein/Ausgabe-Probleme." );
}
}
}

Angst davor, dass wir den Fehlertyp später nicht mehr unterscheiden können, brauchen wir nicht zu haben, denn die an der catch-Anweisung gebundenen Variablen können wir mit instanceof weiter verfeinern. Doch aus Gründen der Übersichtlichkeit sollte diese Technik sparsam angewendet werden. Fehlerarten, die verschieden behandelt werden müssen, verdienen immer getrennte catch-Klauseln. Das trifft zum Beispiel auf FileNotFoundException und IOException zu.

Abbildung


Galileo Computing

7.2.3 Alles geht als Exception durch  downtop

Da Exception die Basisklasse aller Exceptions ist, ließe sich natürlich auch alles mit Exception abfangen. So könnte jemand auf die Idee kommen, aus

try {
irgendwas Unartiges...
}
catch ( IllegalAccessException e ) { ... }
catch ( InstantiationException e ) { ... }

eine Optimierung zu versuchen, die etwa so aussieht:

try {
irgendwas Unartiges...
}
catch ( Exception e ) { ... }

Da der Aufruf in den catch-Blöcken gleich aussieht, ließe sich alles in einer Routine zur Fehlerbehandlung ausführen. Doch dann muss die Oberklasse genommen werden – sozusagen der kleinste gemeinsame Nenner –, und dies ist die Klasse Exception. Doch was für andere Fehlertypen gut funktionieren mag, ist für catch(Exception) gefährlich. Denn so wird wirklich jeder Fehler aufgefangen und in der Ausnahmebehandlung bearbeitet. Taucht beispielsweise eine null-Referenz durch eine nicht initialisierte Variable mit Referenztyp auf, so würde auch das fälschlicherweise behandelt.

Auch Folgendes würde durch das Auffangen einer allgemeinen Exception abgefangen werden. Daher ist das Zusammenfassen zur Oberklasse Exception in der Regel keine gute Lösung:

Point p;
p.setX( 2 ); // Nicht initialisiert
int i = 0;
int x = 12/i; // Division durch Null

Galileo Computing

7.2.4 Ausnahmen, die nicht gefangen werden müssen: RuntimeException  downtop

Einige Fehlerarten können potenziell an vielen Programmstellen auftreten, etwa eine Division durch Null oder ungültige Indexwerte beim Zugriff auf Array-Elemente. Treten solche Fehler beim Programmlauf auf, liegt in der Regel ein Denkfehler des Programmierers vor und das Programm sollte normalerweise nicht versuchen, die ausgelöste Ausnahme aufzufangen und zu behandeln. Daher wurde die Unterklasse RuntimeException eingeführt, die Fehler beschreibt, die vom Programmierer behandelt werden können, aber nicht müssen. Sie werden ausgelöst, wenn der Entwickler beispielsweise doch nicht die Division durch Null verhindert (ArithmeticException) oder die Indexgrenzen missachtet (IndexOutOfBoundsExceptions).

Abbildung


Galileo Computing

7.3 Werfen eigener Exceptions  downtop

Bisher wurden Exceptions lediglich aufgefangen, aber noch nicht selbst erzeugt. Routinen, die durch Exceptions ein Misslingen einer Operation anzeigen, finden sich im Laufzeitsystem oder in der Standardbibliothek zu Genüge. Soll eine Funktion selbst eine Exception auslösen, muss sie ein Exception-Objekt erzeugen und die Ausnahmebehandlung anstoßen. Im Sprachschatz dient das Schlüsselwort throw dazu, eine Ausnahme auszulösen:

public void ichKann( String s )
{
if ( ! kannIchWasMitStringMachen( s ) )
throw new SecurityException ( "Ätsch, das kannst du mit " +s+
" nicht machen!" );
}

Kann mit der übergebenen Zeichenkette s eine bestimmte Operation nicht ausgeführt werden, so wird mit new ein SecurityException-Objekt erzeugt, und diesem eine Zeichenkette als Fehlermeldung mit auf den Weg gegeben. Für nicht passende Werte sieht die Standardbibliothek etwa die Fehlerklasse IllegalArgumentException vor.

Gerne werden Exceptions in den default-Zweig einer switch-Anweisung mit hineingenommen. Im folgenden Beispiel wird versucht, ein Exemplar der Klasse Schokolade mit einer Farbe zu erzeugen. Sollte der Übergabeparameter falsch sein, so wird eine Illegal ArgumentException ausgelöst.

Listing 7.7   Schokolade.java
class Schokolade
{
public final static int WEISS = 0, BRAUN = 1;

private int farbe;

Schokolade( int f )
{
switch( f )
{
case WEISS:
case BRAUN: farbe = f;
break;
default : throw new IllegalArgumentException(
"Falsche Schoko-Farbe: " + f );
}
}

public void test()
{
System.out.println( "Aha, du magst also " +
( ( farbe == WEISS) ? "weisse " : "braune " ) +
"Schokolade gerne!" );
}

public static void main( String args[] )
{
// Schokolade ws = new Schokolade( Schokolade.BRAUN );
Schokolade ws = new Schokolade( 4 );
ws.test();
}
}

Es ist bei diesem Programm deutlich, dass die Fehlerquelle dadurch verringert wird, dass Konstanten die Eigenschaften des Objekts beschreiben. Nach dem Aufruf bekommen wir folgende Meldung:

java.lang.IllegalArgumetException: 
Falsche Schoko-Farbe: 4
at Schokolade.<init>(Schokolade.java:13)
at Schokolade.main(Schokolade.java:28)

Galileo Computing

7.3.1 Vorgefertigte Ausnahme-Objekte wieder verwenden  downtop

Da bei einer Ausnahme ein Exception-Objekt angelegt wird und die übliche Zeit für das Anlegen von Objekten anfällt, lässt sich diese Zeit sparen, in dem eine Referenz auf ein vorgefertigtes Objekt gelegt wird. Hieß es vorher

void foo()
{
...
throw new XYZException();
...
}

so kann es nun wie folgt aussehen:

private static XYZException xyzException 
= new XYZException();
void foo()
{
...
throw xyzException;
...
}

Sinnvoll erscheint dies jedoch nur dann, wenn häufig Ausnahmen ausgelöst werden, was allerdings nicht die Regel sein sollte. Zudem handelt es sich hier um eine Wiederverwendung des Standard-Fehler-Objekts. Parametrisierte Konstruktoren werden oft mit individuellen Werten gefüttert. Hier ist im Vorfeld keine Belegung bekannt, sodass sich das Ausnahme-Objekt nicht anlegen lässt. Ein weiteres Problem ist, dass im Fehlerfall der Stacktrace des Objekts nur die Erzeugungsstelle im Konstruktor bzw. im statischen Initialisierer der umgebenden Klasse angibt, statt der echten Fehlerstelle, von denen es ja unter Umständen viele geben kann.


Galileo Computing

7.3.2 Typecast auf ein null-Objekt für eine NullPointerException  downtop

Mit einem anderen Trick lässt sich die manuelle Objekterzeugung beim Auslösen einer Exception umgehen. Werfen wir ein Blick auf folgendes Codebeispiel:

Listing 7.8   ThrowNull.java
class ThrowNull
{
static void bar()
{
throw
(RuntimeException) null;
}

public static void main( String args[] )
{
bar();
}
}

Das Programm kompiliert und löst eine NullPointerException aus.

java.lang.NullPointerException
at ThrowNull.bar(ThrowNull.java:5)
at ThrowNull.main(ThrowNull.java:10)

Da wir null auf RuntimeException casten, müssen wir auch kein throws in der Methodendeklaration angehen und den Fehler auch nicht bei bar() behandeln. Das ist aber ein recht fragwürdiger Stil, da hier das hierarchische Typsystem für Fehlerarten unterlaufen wird und alle Fehler zur NullPointerException werden.


Galileo Computing

7.3.3 Neue Exception-Klassen definieren  downtop

Eigene Fehlerarten werden definiert, indem die Klasse Exception in einer Unterklasse erweitert wird. Dabei werden oft zwei Konstruktoren verwendet, ein Standard-Konstruktor und ein zweiter mit einem String als Parameter. Um für die Klasse Schokolade im letzten Beispiel einen neuen Fehlertyp zu definieren, erweitern wir IllegalArgumentException zur illegalen Schoko-Farbe.

Listing 7.9   IllegalSchokoColorException.java
public class IllegalSchokoColorException
extends IllegalArgumentException
{
public IllegalSchokoColorException()
{
super();
}

public IllegalSchokoColorException( String s )
{
super( s );
}
}

Nehmen wir uns die Abfrage noch einmal vor, dann wird an Stelle der vorherigen Illegal ArgumentException eine IllegalSchokoColorException ausgelöst:

throw new IllegalSchokoColorException( 
"Diese Schokoladen-Farbe"+
" gibt es nicht: " + f );
Abbildung

Im Hauptprogramm können wir auf diese Ausnahme reagieren: Entweder wir fangen in einem Aufruf IllegalSchokoColorException ab oder wir lassen die spezielle IllegalArgumentException, die ja eine RuntimeExeption ist und daher nicht abgefangen werden braucht, die JVM beenden. Um die alte Schokoladen-Klasse mit der IllegalArgumentexception von der neuen mit der eigenen Fehlerklasse IllegalSchokoColorException zu trennen, nennen wir die neue Klasse Schokolade2.

Listing 7.10   Schokolade2.java
 public static void main( String 
args[] )
{
try
{
Schokolade2 ws = new Schokolade2( 4 );
ws.test();
}
catch( IllegalSchokoColorException isce )
{
System.err.println( "Falsche Schokoladen-Farbe abgefangen" );
isce.printStackTrace();
}

System.err.println();
Schokolade2 ws1 = new Schokolade2(3);
ws1.test(); // Abbruch duch IllegalSchokoColorException
}
}

Die erwartete Ausgabe ist:

Falsche Schokoladen-Farbe abgefangen
IllegalSchokoColorException: Diese Schokoladen-Farbe gibt es nicht: 4
at Schokolade2.<init>(Schokolade2.java:26)
at Schokolade2.main(Schokolade2.java:40)

IllegalSchokoColorException: Diese Schokoladen-Farbe gibt es nicht: 3
at Schokolade2.<init>(Schokolade2.java:26)
at Schokolade2.main(Schokolade2.java:50)
Exception in thread "main"

Galileo Computing

7.4 Rückgabewerte bei ausgelösten Ausnahmen  downtop

Java versucht durch Flussanalyse den Programmablauf innerhalb einer Methode zu bestimmen, und zu melden, ob definitiv ein Rückgabewert geliefert wird. Dabei werden die Programmpfade verfolgt und Ausdrücke unter Umständen ausgewertet. Doch die Aussage: Jede Funktion mit Ergebnistyp ungleich void muss eine return-Anweisung besitzen, müssen wir etwas relativieren. Nur in einem speziellen Fall müssen wir dies nicht. Nämlich genau dann, wenn vor dem Ende der Funktion eine throws-Anweisung die Abarbeitungsreihenfolge beendet. Schauen wir uns drei Methoden an:

Listing 7.11   NoReturn.java
class NoReturn
{
int foo()
{
throw new RuntimeException();
}

void bar()
{
if ( true )
throw new RuntimeException();
}

int zof()
{
if ( true ) // while würde statt dessen gehen!
throw new RuntimeException();
}
}

Einen Blick auf foo() verrät, dass trotz Rückgabewert keine return-Anweisung eingesetzt wird. Die Abarbeitung wird vor dem Rücksprung durch eine Exception abgebrochen. foo() muss diese Exception nicht mit throws ankündigen, da wir wieder ein Exemplar von RuntimeException erzeugen. Bei bar() führt nur eine wahre Bedingung zu einem Abbruch. Da if(true) immer wahr ist, wird die Methode mit einer Exception beendet. Einen Rückgabewert haben wir nicht, daher ist es egal, ob wir ein return einsetzten oder nicht. Interessanter ist da schon die Methode zof(). Wie bei foo() haben wir einen vorgeschriebenen Rückgabewert, aber eine Exception soll dies überflüssig machen. In unserem Gedanken wird if(true) immer ausgeführt und die Exception wird immer ausgelöst, wie im Fall bar(). Das erkennt der Compiler allerdings nicht und meckert, eine return-Anweisung einzusetzen. Die Flussanalyse geht nicht so weit, den konstanten Ausdruck auszuwerten. Paradoxerweise würde ein while(true) hier funktionieren und auch zof() übersetzungsfähig machen.


Galileo Computing

7.5 Ein Assert in Java  downtop

Die Programmierer unter C(++) haben mitunter eine Funktion oder ein Makro assert() lieb gewonnen. Dies prüft eine Bedingung und bricht das Programm ab, wenn sie nicht zutrifft. Damit können Zusicherungen geschaffen werden. Sind diese nicht erfüllt, kann die Funktion nicht arbeiten. Dies bringt die Softwareentwicklung in die Richtung der Vertragsbasierten Programmierung. Die Software und der Anwender gehen einen Vertrag ein. Hält sich der Entwickler nicht an den Vertrag, in dem er beispielsweise einer Funktion ungeeignete (verbotene) Parameterwerte übergibt, wird der Vertrag gebrochen und über assert zum Beispiel ein Fehler gemeldet.

Unter C(++) wird assert() als Makro implementiert, welches bei der Compilierung zu einer if-Anweisung ausgebaut wird. Mithilfe der Funktion lassen sich Laufzeitzeitfehler abfangen. Das schöne unter C(++): Wenn die Präprozessordefinition NDEBUG gesetzt ist, entfernt der Präprozessor sämtliche Aufrufe von assert(). So erhält man, nach erfolgreichem Software-Test, eine schnellere Programmversion, die keine (überflüssigen) Überprüfungen der Zusicherungen mehr vornimmt.

Damit die Assert-Funktionalität in Java genutzt werden kann, bieten sich zwei Realisierungen an. Zum einen bietet Java ab der Version 1.4 eine Assert-Möglichkeit auf Compiler-Ebene an. Zum anderen können wir diese praktische Funktion einfach in eine Klasse packen. Wir wollen den zweiten Weg gehen.

Die Methode assert() ist mehrfach überladen, damit sie unter verschiedenen Bedingungen aufgerufen werden kann. Damit nicht erst ein Assert-Objekt erzeugt werden muss, ist die Funktion static gekennzeichnet. Zusätzlich wollen wir unser assert() noch erweitern, denn es kann zusätzlich die Aufrufreihenfolge auflisten, die sich durch den Aufruf-Stack ergibt.

Listing 7.12   Assert.java
public class Assert
{
private static void fail( String s )
{
System.err.println( "Assertion failed on " + s );

Throwable e = new Throwable();
e.printStackTrace();

System.exit( 1 );
}

public static void assert( boolean aBoolean ) {
if ( !aBoolean )
fail( "Boolean" );
}

public static void assert( char aChar ) {
if ( aChar == '\0' )
fail( "Char" );
}

public static void assert( long aLong ) {
if ( aLong == 0L )
fail( "Long" );
}

public static void assert( double aDouble ) {
if ( aDouble == 0.0 )
fail( "Double" );
}

public static void assert( Object anObject ) {
if ( anObject == null )
fail( "Object" );
}
}

Immer dann, wenn assert() bemüht wird, ruft dies wiederum im Fehlerfall fail() auf. Dies erzeugt seinerseits die Ausgabe des Stacks über das Throwable-Objekt, welches eine printStackTrace()-Methode versteht.

class java.lang.Throwable
implements Serializable

gp  void printStackTrace()
Schreibt das Throwable und anschließend den Stack-Inhalt in den Standard-Ausgabe-Strom.
gp  void printStackTrace( PrintStream s )
Schreibt das Throwable und anschließend den Stack-Inhalt in den angegebenen PrintStream.
gp  void printStackTrace( PrintWriter s )
Schreibt das Throwable und anschließend den Stack-Inhalt in den angegebenen PrintWriter.

Schreiben wir nun ein Beispielprogramm, welches das Verhalten ausnutzt:

Listing 7.13   AssertTest.java
class AssertTest
{
public static void main( String args[] )
{
int i = 0;
Assert.assert( i > 10 );
}
}

Dieses kleine Programm erzeugt dann etwa folgende Stack-Ausgabe:

Assertion failed on Boolean
java.lang.Throwable
at Assert.fail(AssertTest.java:10)
at Assert.assert(AssertTest.java:18)
at AssertTest.main(AssertTest.java:105)

Sehr schön deutlich sind die Funktionsaufrufe sichtbar. Dazu noch mit den Nummern der Fehlerzeilen.

Stack-Ausgabe neu formatieren

Die Ausgabe der ersten Zeile (java.lang.Throwable) lässt sich nur dann vermeiden, wenn der Aufruf von printStackTrace() mit einem Stream oder Writer als Parameter geschieht und dann umformatiert wird. An dieser Stelle greifen wir etwas vor.

Die Implementierung von printStackTrace() schreibt die Ausgabe inklusive der ersten Zeile auf System.err. (Die printStackTrace0()-Methode ist im Übrigen privat und nativ.)

void printStackTrace()
{
synchronized ( System.err )
{
System.err.println( this );
printStackTrace0( System.err );
}
}

Auch bei einem PrintStream beziehungsweise PrintWriter, wird das this-Objekt ausgegeben, nur dann in den Stream bzw. Writer hinein. Wollen wir eine benutzerdefinierte Ausgabe erreichen, die später zum Beispiel geparst werden kann, so müssen wir eine dieser Methoden benutzen. So lässt sich die Ausgabe auch in einen StringWriter leiten und dann verarbeiten. Ändern wir die Methode fail() nun so, dass sie nur die wirkliche Fehlerzeile ausgibt und auf die ersten vier Zeilen – die ja unsere Implementierung verraten – verzichtet:

private static void fail( String 
s )
{
System.err.println( "Assertion failed on " + s );

StringWriter sw = new StringWriter();

Throwable e = new Throwable();
e.printStackTrace( new PrintWriter( sw ) );

StringTokenizer st =
new StringTokenizer( sw.toString(), "\n");

// Don't think about time and space
st.nextToken(); st.nextToken(); st.nextToken();

while ( st.hasMoreTokens() )
System.out.println(
" " + ( (String) st.nextToken() ).trim().substring(3) );

System.exit( 1 );
}

Die Ausgabe ist dann bei einer Fehlerzeile etwas kompakter. Betrachten wir dazu das folgende Test-Programm:

Listing 7.14   AssertTest.java
class AssertTest
{
static void dummfug()
{
double d = 0.0;
Assert2.assert( d );

int i = 0;
Assert.assert( i < 10 );
}

public static void main( String args[] )
{
dummfug();
}
}

Assert2 ist die Klasse mit der erweiterten Fehlerbehandlung. Dann erscheint auf dem Bildschirm:

Assertion failed on Double
AssertTest.dummfug(AssertTest.java:96)
AssertTest.main(AssertTest.java:104)

Natürlich müssen wie diesen Hack mit Vorsicht genießen. Das Format der printStackTrace()-Ausgabe ist nicht standardisiert und das Analysieren der Ausgabe problematisch. Es können etwa einige Zwischenstufen oder die Zeilennummern bei optimierender JIT-Übersetzung fehlen.

Nun könnte das Originalverhalten auch abgebildet werden, denn assert() unter C(++) gibt eine wohldefiniert aufgebaute Meldung aus:

Assertion failed: test, file Dateiname, 
line Zeilennummer

Der Dateiname ist der Name der Quelldatei und die Zeilennummer ist die Zeilennummer, in der das Makro erscheint.


Galileo Computing

7.6 Sicherheitsfragen mit dem SecurityManager klären  downtop

In der Laufzeitumgebung sind spezielle Ausnahmen für den Fall definiert, dass das System Operationen nicht zulässt, die das gerade laufende Programm gerne ausführen möchte. In einem Applet lassen sich beispielsweise keine Dateien löschen, da ein Sicherheitsmanager dies verhindert. In dem Fall, dass der Applet-Programmierer einen Zugriff auf das Datei-System versucht, wird im Applet eine SecurityException vom Sicherheitsmanager ausgelöst.


Galileo Computing

7.6.1 Programm beenden  toptop

Die statische Methode exit() aus der Systemklasse gibt den Wunsch, die JVM komplett zu beenden, an die Laufzeitumgebung weiter, die durch ein Runtime-Objekt repräsentiert ist. So delegiert die Implementierung von exit() der Klasse System die ganze Arbeit direkt an die Methode exit() der Klasse Runtime:

public static void exit(int status) 
{
Runtime.getRuntime().exit(status);
}

Dürfen wir Beenden? Ein detaillierter Blick auf die Implementierung

In der Methode exit() von Runtime taucht die Frage auf, ob ein Programm die gesamte JVM beenden darf. Über den SecurityManager folgt die Anfrage mit checkExit(), ob die Berechtigung zum Beenden der Laufzeitumgebung existiert. Wenn wir die JVM schließen dürfen, beendet die native Methode exitInternal(int) die Anwendung. Es ist die Aufgabe von exitInternal(), die von der JVM benutzen Ressourcen freizugeben:

public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExit(status);
exitInternal(status);
}

Applets dürfen zum Beispiel nicht die gesamte JVM beenden, da alle Applets auf derselben Webseite in derselben JVM ablaufen und gegeneinander abgeschirmt werden müssen. Für Applets liefert getSecuritymanager() eine Referenz ungleich null und die check Exit-Methode() dieses SecurityManager-Exemplars löst eine SecurityException aus, um dem Applet das Beenden der JVM zu verbieten. Natürlich dürfen Applets auch den SecurityManager nicht mit setSecurityManager() verändern und so den Schutz aushebeln.

  

Java 2




Copyright © Galileo Press GmbH 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de