Previous Page TOC Index Next Page See Page

Ausnahmeverarbeitung

Manchmal können auch in Anwendungen, für die ein ausreichendes Debugging und Testen stattgefunden hat, Probleme auftreten, die das Programm selbst nicht mehr bewältigen kann. Solche Problemen können durch die Umgebung verursacht werden, in der Ihr Programm ausgeführt wird, oder der Anwender macht etwas, was Ihr Programm nicht versteht. Egal, um welches Problem es sich handelt, eine gute Anwendung sollte in der Lage sein, das Unerwartete zu handhaben oder wenigstens korrekt beendet zu werden.


Eine Ausnahme ist ein Ereignis oder eine Bedingung, die den normalen Programmfluß unterbricht.

Bevor die Programmiersprachen eine Ausnahmeverarbeitung boten, waren die Programmierer gezwungen, alle möglichen Bedienfehler und Umgebungsbedingungen im voraus abzusehen. Das führte zu den allgegenwärtigen Funktionen zur Fehlerbehandlung, die etwa wie folgt aussahen:

int status = callSomethingThatAlmostAlwaysWorks();
if (status == ABNORMAL_RETURN_VALUE) {
. . . // irgend etwas ist passiert - > Lösung suchen
switch(someGlobalErrorIndicator) {
. . . // potentielles Problem 1 lösen
. . . // potentielles Problem 2 lösen
. . . // potentielles Problem 3 lösen
. . .
. . . // potentielles Problem n lösen
}
else {
. . . // alles in Ordnung, viel Spaß noch
}
}

Ein solcher Codeabschnitt konnte relativ lang werden, abhängig davon, wie sorgfältig die Entwickler sein wollten. Neben der Arbeit, die durch die Verarbeitung des noch so seltenen Falls entstand, ging damit auch die Logik für die Fehlerverarbeitung direkt in die Programmlogik ein, was den Code schwer lesbar und wartbar machten. Die Verarbeitung vieler Fehler war schwierig, wenn nicht gar unmöglich. Die einzige Möglichkeit war manchmal, zu versuchen, das Programm zu beenden und dem Anwender einen Neustart zu überlassen.

Mit der Ausnahmeverarbeitung wurde eine Lösung für die meisten dieser Probleme gefunden. Ausnahmen ermöglichen es Ihrem Programm, sich mit dem Ausnahmefall zu beschäftigen, und häufig kann Ihr Programm einfach nur einen anderen Code ausführen, statt einfach herunterzufahren. Die Logik für die Fehlerverarbeitung wird in die Methode zur Ausnahmeverarbeitung ausgelagert und in Ihrem Code durch einen einzigen Methodenaufruf ersetzt, so daß dieser einfacher lesbar und wartbar wird. Viele Fehler können effizient verarbeitet werden, und Sie haben die Möglichkeit, Aufräumarbeiten vorzunehmen, bevor das Programm geschlossen wird.

Heute lernen Sie die Ausnahmen in Java kennen, Instanzen der Klasse Throwable (oder einer ihrer Unterklassen):

Ausnahmen im Design

Wenn Sie anfangen, komplexe Programme in Java zu entwickeln, werden Sie feststellen, daß Sie nach dem Design der Klassen und Schnittstellen und ihrer Methodenbeschreibungen noch nicht alle Verhalten Ihrer Objekte definiert haben. Schließlich beschreibt die Schnittstelle nur die normale Verwendung eines Objekts, und nicht die ungewöhnlichen, die Ausnahmefälle. In vielen Fällen übernimmt die Dokumentation diese Aufgabe, indem sie explizit die Situationen beschreibt, die in den Methoden zur Fehlerverarbeitung berücksichtigt werden, wie in der Einführung zu diesem Kapitel bereits gezeigt. Weil das System nichts über diese Methoden weiß, kann es aber auch keine Prüfung auf Konsistenz vornehmen. Der Compiler kann Ihnen bei diesen Ausnahmebedingungen überhaupt nicht helfen, im Gegensatz zu den hilfreichen Warnungen und Fehlern, die er erzeugt, wenn eine in der Sprache definierte Methode fehlerhaft verwendet wird.

Was noch wichtiger ist, Sie haben diesen wichtigen Aspekt Ihres Programms beim Design nicht berücksichtigt. Statt dessen sind Sie gezwungen, eine Möglichkeit zu finden, ihn in der Dokumentation zu beschreiben, und hoffen, bei der Implementierung keine Fehler gemacht zu haben. Und noch schlimmer, es gibt keinen Standard, diese Ausnahmefälle zu verarbeiten, d.h. jeder Java-Programmierer müßte eine eigene Methode finden, dieselben »Umstände« zu beschreiben. Sie brauchen also eine einheitliche Vorgehensweise, die Arbeitsweise Ihrer Klassen und Methoden im Hinblick auf diese Ausnahmefälle zu beschreiben.

Man könnte sich eine Methodenbeschreibung auch als Vertrag zwischen dem Entwickler einer Methode (oder Klasse) und dem Aufrufer der Methode vorstellen. In der Regel zeigt diese Beschreibung, welche Typen die Argumente einer Methode haben, was sie zurückgibt, und die allgemeine Semantik. Jetzt kann sie aber auch den Aufrufer benachrichtigen, welche »unnormalen« Dinge sie machen kann. Wie der Rückgabewert einer Methode mit einem bestimmten Typ deklariert ist, hilft dies, explizit alle Stellen zu finden, wo Ausnahmebedingungen in Ihrem Programm verarbeitet werden sollen, was ein umfangreiches Design sehr viel einfacher macht.

Weil es sich bei Ausnahmen um Instanzen von Klassen handelt, können sie in eine Hierarchie eingeordnet werden, die ihre Beziehungen zueinander beschreibt. Wenn Sie die Klassenhierarchie in der Hilfe (Java-Referenz) betrachten, sehen Sie, daß die Throwable-Klasse zwei große Klassenhierarchien unter sich hat, nämlich Error und Exception. Diese Hierarchien verkörpern die zahlreichen Beziehungen, die zwischen Ausnahmen und Fehlern in der Java-Laufzeitumgebung bestehen können.

Das Schüsselwort throws

Wenn Sie wissen, daß eine bestimmte Art Fehler oder Ausnahme in Ihrer Methode auftreten kann, können Sie diesen entweder explizit verarbeiten oder mögliche Aufrufer (Klasseen oder Methoden, die Ihre Methode aufrufen) mit Hilfe der throws-Klausel warnen. Hier ein kurzes Beispiel zur Verwendung des Schlüsselworts throws:

public class MyFileClass {
. . .
public void aClassyMethod() throws EOFException,
FileNotFoundException {
. . .
}
}

Hier benachrichtigen Sie den Compiler (und die Leser Ihres Codes), daß der Code in aClassyMethod() die vordefinierten Java-Ausnahmen EOFException und FileNotFoundException aufwerfen kann. Die Ausnahme FileNotFoundException beispielsweise wird benötigt, weil diese Methode versucht, ein neues FileInputStream-Objekt zu erzeugen, indem sie eine bestimmte Datei angibt, von der man nicht weiß, ob sie existiert. Durch die Information, daß diese Methode die Ausnahme FileNotFoundException aufwerfen kann, teilen Sie dem Compiler mit, daß Ihre Methode Code verwendet, der diese Art Ausnahme erzeugen kann.

Wenn Code geschrieben wird, der aClassyMethod() aufruft, stellt dieser Mechanismus sicher, daß der Code entweder die Ausnahme auffängt, oder daß er ebenfalls eine Ausnahme aufwirft. Wenn das nicht der Fall ist, beschwert sich der Compiler, daß sich niemand um die Ausnahme kümmert, für die der Code verantwortlich ist.

Weil Ausnahmen Objekte in einer Klassenhierarchie sind, können Sie sie auch nach ihren Oberklassen in gruppieren. In dem obigen Beispiel sind EOFException und FileNotFoundException Unterklassen von IOException, deshalb hätten Sie folgendes tun können:

public void aClassyMethod() throws IOException {...}

Dann hätten Sie beide Ausnahmetypen gleichzeitig mit ihrer Oberklasse IOException abgedeckt. Das hindert Sie jedoch daran, die einzelnen Ausnahmetypen in dem Code, der aClassyMethod() aufgerufen hat, separat zu verarbeiten.

Nicht alle Fehler und Ausnahmen müssen aufgelistet werden; in Ihren throws-Klauseln müssen nicht alle Instanzen von Error oder RuntimeException (oder ihren Unterklassen) enthalten sein. Sie werden gesondert behandelt, weil sie überall in einem Java-Programm auftreten können. Ein gutes Beispiel dafür ist OutOfMemoryError, der jederzeit und überall und aus den unterschiedlichsten Gründen auftreten kann.


Sie können diese Fehler und Laufzeitausnahmen natürlich in Ihrer throws-Klausel auflisten, wenn Sie das möchten, aber die Aufrufer Ihrer Methoden sind nicht gezwungen, sie zu verarbeiten; nur Nicht-Laufzeitausnahmen müssen verarbeitet werden.

Mit dem Ausschluß von Fehlern und Laufzeit-Ausnahmen gibt es sechs Arten von Ausnahmen in java.lang, die in einer throws-Klauses aufgelistet werden müssen:

Es gibt auch Ausnahmen, die in anderen Paketen deklariert sind. In java.io beispielsweise ist die Klasse IOException definiert, deren Ausnahme-Unterklassen mehreren verschiedenen Paketen angehören:

Außerdem ist in java.awt die Klasse AWTException definiert. Immer wenn Sie eine Java-Methode verwenden, die eine dieser Ausnahmen aufwirft, müssen Sie diese Ausnahme in der throws-Klausel Ihrer Methode angeben..

Die Java-Klassenbibliothek verwendet Ausnahmen überall und mit viel Erfolg. Wenn Sie die detaillierte API-Dokumentation in der Hilfe von JBuilder betrachten (Java-Referenz), sehen Sie, daß viele der Methoden in der Bibliothek throws-Klauseln haben, und einige davon dokumentieren es sogar, wenn bestimmte Fehler oder Laufzeitausnahmen auftreten können (und wenn das für die Situation wichtig sein könnte). Das ist nett von demjenigen, der die Dokumentation verfaßt hat, weil Sie ja diese impliziten Bedingungen nicht auffangen müssen. Wenn es nicht offensichtlich war, daß eine solche Bedingungen irgendwo und aus irgendeinem Grund auftreten könnte, dann ist das wirklich eine praktische Information.

Die Verwendung von Ausnahmen

Nachdem Sie ein Gefühl dafür erhalten haben, wie Ausnahmen beim Design eines Programms helfen können, sollten Sie erfahren, wie Ausnahmen eingesetzt werden. Wir wollen eine neuen Klasse erzeugen, die eine Methode enthält, die eine Ausnahme aufwirft.

public class MyFirstClass {
. . .
public void aSpecialMethod() throws MyFirstException {
. . . // hier wird etwas gearbeitet
}
}

An irgendeiner anderen Stelle könnten Sie eine weitere Methode definieren

public void anotherSpecialMethod() throws MyFirstException {
MyFirstClass aMFEC = new MyFirstClass()
. . .
aMFEC.aSpecialMethod();
}

Nun wollen wir diesen Code genauer betrachten. Angenommen, MyFirstException ist eine Unterklasse von Exception, dann heißt das, wenn Sie sie nicht im Code von anotherSpecialMethod() verarbeiten, müssen Sie alle Aufrufer dieser Methode warnen. Weil Ihr Code einfach nur aSpecialMethod() aufruft, ohne Angaben darüber zu machen, daß sie eine MyFirstException aufwerfen könnte, müssen Sie diese Ausnahme der throws-Klausesl von anotherSpecialMethod() hinzufügen. Das ist völlig legal, aber es stört den Aufrufer mit etwas, was Sie vielleicht selbst erledigen hätten sollen. Das hängt natürlich von den Umständen ab.

Angenommen, heute haben Sie ein gewisses Verantwortungsgefühl und beschließen, die Ausnahme selbst zu verarbeiten. Sie deklarieren jetzt die Methode ohne eine throws-Klausel, d.h. Sie müssen irgend etwas mit der erwarteten Ausnahme machen:

public void aResponsibleMethod() {
MyFirstClass aMFEC = new MyFirstClass()
. . .
try {
aMFEC.aSpecialMethod();
}
catch (MyFirstException mfe) {
. . . // irgend eine Verarbeitung
}
}

Hier werden zwei neue Schlüsselwörter eingeführt: try und catch. Die try-Anweisung sagt im wesentlichen »Versuche, den Code in diesem Block auszuführen. Wenn Ausnahmen auftreten, dann verarbeiten wir sie.« Die catch-Anweisung sagt »Ich fange diese Ausnahme auf und mache etwas damit.« Sie können beliebig viele catch-Anweisungen bereitstellen.

try und catch

Im try-Block sollten Sie allen Code unterbringen, der eine Ausnahme aufwerfen könnte. Wenn Sie eine Java-Methode zum ersten Mal einsetzen, sollten Sie ihre Definition in der Hilfe oder im Quellcode nachschlagen (falls das zur Verfügung steht): Suchen Sie in der Methodensignatur nach der throws-Klausel. Wenn die Methode eine Ausnahme aufwirft, müssen Sie entscheiden, ob Sie diese in die throws-Klausel Ihrer Methode aufnehmen, oder ob Sie sie verarbeiten. Wenn Sie sie verarbeiten, schließen Sie den Aufruf dieser Methode in einen try-Block ein.

Im catch-Block können Sie alle Ausnahmen verarbeiten, die in der Argumenteliste angegeben sind. Dabei werden alle Instanzen einer angegebenen Klasse oder all ihrer Unterklassen und alle Klassen, die eine angegebene Schnittstelle implementieren, berücksichtigt. In der catch-Anweisung des obigen Beispiels werden Ausnahmen der Klasse MyFirstException (und all ihrer Unterklassen) verarbeitet.

Das Schlüsselwort throw

Was tun Sie, wenn Sie beide vorgestellten Ansätze kombinieren wollen? Sie wollen die Ausnahme selbst verarbeiten, sie aber auch dem Aufrufer Ihrer Methode anzeigen. Das kann realisiert werden, indem die Ausnahme explizit erneut aufgerufen wird, nämlich mit dem Schlüsselwort throw:

public void responsibleExceptMethod() throws MyFirstException {
MyFirstClass aMFEC = new MyFirstClass()
. . .
try {
aMFEC.aSpecialMethod();
}
catch (MyFirstException mfe) {
. . . // irgendeine Verarbeitung
throw mfe; // Ausnahme erneut aufwerfen
}
}

Das funktioniert, weil Ausnahmeverarbeitungen verschachtelt werden können. Angenommen, Sie verarbeiten die Ausnahmen, indem Sie irgendeine Aktion vornehmen, aber dann feststellen, daß es so wichtig ist, daß Ihr Aufrufer es auch verarbeiten muß. Ausnahmen werden auf diese Weise die Kette der Methodenaufrufer nach oben durchgereicht (und in der Regel von den meisten davon nicht verarbeitet), bis sie schließlich zum System kommen, das alle nicht aufgefangenen Ausnahmen verarbeitet, indem es Ihr Programm abbricht und eine Fehlermeldung ausgibt. In einer Standalone-Anwendung ist das nicht verkehrt, aber in einem Applet kann es den Absturz des Web-Browsers verursachen. Die meisten Web-Browser schützen sich gegen diese Katastrophe, idnem sie alle von einem Applet erzeugten Ausnahmen selbst auffangen, wenn sie ein Applet ausführen, aber darauf kann man sich nicht verlassen. Wenn Sie die Möglichkeit haben, eine Ausnahme aufzufangen und etwas sinnvolles dagegen zu tun, dann sollten Sie das auch.

Mit dem Schlüsselwort throw können Sie eine neue Ausnahme initiieren. Betrachten wir noch einmald die Definition von MyFirstClass und bauen wir sie ein bißchen aus:

public class MyFirstClass {
. . .
public void aSpecialMethod() throws MyFirstException {
. . .
if (someUnusualThingHappened()) {
throw new MyFirstException();
// hier kommt die Ausnahme nie vorbei
. . .
}
}
}


throw verhält sich wie eine break-Anweisung - alles, was dahinter steht, wird niemals ausgeführt.

Auf diese grundsätzliche Weise werden alle Ausnahmen erzeugt - irgendjemand muß irgendwo ein Ausnahme-Objekt erzeugen und es aufwerfen. Die gesamte Hierarchie unter Throwable wäre jedoch zu wenig nütze, wenn es nicht throw-Anweisungen im Code der Java-Bibliothek genau an den richtigen Stellen gäbe. Weil Ausnahmen aus jeder Tiefe einer Methode nach oben gereicht werden, kann jeder Methodenaufruf eine Unmenge möglicher Fehler und Ausnahmen verursachen. Glücklicherweise müssen nur diejenigen berücksichtigt werden, die in der throws-Klausel einer Methode aufgelistet sind, und der Rest kann stillschweigend weitergereicht werden und zu einer Fehlermeldung werden (wenn er nicht weiter oben im System aufgefangen wird).

Hier folgt eine eigenwillige Demonstration dieses Sachverhalts, wo die Ausnahme, die aufgeworfen wird, und der Handler, der sie auffängt, sehr eng beieinander liegen:

System.out.print("Jetzt ");
try {
System.out.print("ist ");
throw new MyFirstException();
System.out.print("eine ");
}
catch (MyFirstException m) {
System.out.print("die richtige ");
}
System.out.println("Zeit.");

Die Ausgabe dieses Codes zeigt den Steuerfluß im Code:


Jetzt ist die richtige Zeit.


Durchfallen

Ausnahmen stellen einen sehr leistungsfähigen Mechanismus dar, alle möglichen Fehlerbedingungen in überschaubare Stücke zu unterteilen. Weil der erste catch-Block, für den eine Übereinstimmung festgestellt wird, ausgeführt wird, können Sie ganz leicht Ketten wie die folgende erzeugen:

try {
someReallyExceptionalMethod();
}
catch (NullPointerException n) {
. . . // Unterklassen von RuntimeException
}
catch (RuntimeException r) {
. . . // Unterklassen von Exception
}
catch (MyFirstException m) {
. . . // Ihre Unterklasse von Exception
}
catch (IOException i) {
. . . // Unterklasse von Exception
}
catch (Exception e) {
. . . // Unterklasse von Throwable
}
catch (Throwable t) {
. . . // Error-Klasse-Fehler und alles andere, was noch nicht
. . . // in den vorigen catch-Blöcken verarbeitet wurde
}

Durch Auflistung der Unterklassen vor ihren Elternklassen und der angepaßten Ausnahmen vor den Standard-Ausnahmen erzeugen Sie eine Hierarchie für die Fehlerverarbeitung, die erlaubt, daß die spezifischeren Ausnahmen zuerst versuchen, das Problem zu lösen, und die allgemeineren Klassen fangen auf, was hier nicht berücksichtigt wird, also »durchfällt«. Durch die Aneinanderreihung von Ketten wie diesen können Sie fast jede Kombination von Überprüfungen realisieren. Wenn etwas wirklich Außergewöhnliches auftritt, was Sie nicht berücksichtigen können, dann können Sie möglicherweise eine Schnittstelle verwenden, um es zu verarbeiten. Das würde Ihnen ermöglichen, eine ganz eigene Ausnahmehierarchie zu erzeugen, die eine Mehrfachvererbung simuliert. Eine Schnittstelle aufzufangen, statt einer Klasse, kann auch für einen Test auf eine Eigenschaft verwendet werden, die viele Ausnahmen gemeinsam nutzen, die aber im Einfachvererbungsbaum nicht ausgedrückt werden kann.

Angenommen, einige Ihrer Ausnahmeklassen machen ein Neubooten erforderlich, wenn sie aufgeworfen wurden. Sie erzeugen die Schnittstelle NeedsReboot, und alle diese Klassen implementieren diese Schnittstelle. (Keine davon muß eine gemeinsame Eltern-Ausnahmeklasse haben.) Die höchste Ebene der Ausnahme-Hanlder fängt dann einfach die Klasseen auf, die NeedsReboot implementieren, und führt diese Aufgabe aus:

public interface NeedsReboot { } // braucht gar keinen Inhalt zu haben

try {
someMethodThatGeneratesExceptionsThatImplementNeedsReboot();
}
catch (NeedsReboot n) { // Schnittstelle auffangen
. . . // cleanup
SystemClass.reboot(); // Neubooten mit Hilfe einer fertigen Systemklasse
}

Übrigens, wenn Sie wirklich ein ungewöhnliches Verhalten für eine Ausnahme brauchen, dann können Sie dieses auch in der Ausnahmeklasse selbst realisieren! Sie wissen, daß eine Ausnahme eine ganz normale Klasse ist, die also auch Instanzvariablen und -Methoden beinhalten kann. Diese Verwendung ist zwar etwas ungewöhnlich, abe rin einigen Situationen kann sie durchaus sinnvoll sein. Und so könnte das aussehen:

try {
someExceptionallyStrangeMethod();
}
catch (ComplexException e) {
switch (e.internalState()) { // kann eine Instanzvariable zurückgeben
case e.COMPLEX_CASE: // Klassenvariable der Ausnahmeklasse
e.performComplexBehavior(myState, theContext, ...);
break;
. . . // der ganze Rest der Prüfung
}
}

Der finally-Block

Schließlich wollen wir noch den finally-Block betrachten. Angenommen, es gibt eine Operation, die unbedingt ausgeführt werden muß, egal, was passiert. Dabei geht es in der Regel darum, bestimmte Ressourcen freizugeben, eine Datei zu schließen oder etwas ähnliches. Um sicherzugehen, daß »egal, was passiert« auch für Ausnahmen gilt, verwenden Sie den finally-Block, der genau für diese Situation entwickelt wurde:

SomeFileClass f = new SomeFileClass();
if (f.open("/a/path/name/file")) {
try {
someReallyExceptionalMethod();
}
finally {
f.close();
}
}

Das ist ungefähr dasselbe, als ob Sie folgendes geschrieben hätten:

SomeFileClass f = new SomeFileClass();
if (f.open("/a/path/name/file")) {
try {
someReallyExceptionalMethod();
}
catch (Throwable t) {
f.close();
throw t;
}
}

Der einzige Unterschied ist, daß finally auch verwendet werden kann, um Aufräumarbeiten auszuführen, und nicht nur nach Ausnahmen, sondern auch nach return-, break- und continue-Anweisungen. Listing 13.1 zeigt diese Möglichkeiten.

Listing 13.1: MyExceptionalClass.java

1: public class MyExceptionalClass {
2: public static void main(String args[]) {
3: int mysteriousState = Integer.parseInt(args[0]);
4: while (true) {
5: System.out.print("Who ");
6: try {
7: System.out.print("is ");
8: if (mysteriousState == 1)
9: return;
10: System.out.print("that ");
11: if (mysteriousState == 2)
12: break;
13: System.out.print("strange ");
14: if (mysteriousState == 3)
15: continue;
16: System.out.print("but kindly ");
17: if (mysteriousState == 4)
18: throw new UncaughtException();
19: System.out.print("not at all ");
20: }
21: finally {
22: System.out.print("amusing man?\n");
23: }
24: System.out.print("I'd like to meet the man.");
25: }
26: System.out.print("Please tell me.\n");
27: }
28: }


Die Ausgabe, die hier erzeugt wird, hängt von dem Wert von mysteriousState ab. Wenn mysteriousState gleich 1 ist, entsteht die folgende Ausgabe:


Who is amusing man?


Hier berücksichtigt die Ausgabe die print()-Anweisung vor der return-Anweisung. Die return-Anweisung wird »gehalten«, während die print()-Anweisung im finally-Block ausgeführt wird. Anschließend wird die main()-Methode verlassen. Für mysteriousState=2 gilt:


Who is that amusing man?
Please tell me.


Die print()-Anweisungen vor der break-Anweisung werden ausgeführt, dann die print()-Abbildung im finally-Block, gefolgt von der print()-Anweisung nach dem Ende der while-Schleife. Für mysteriousState=3 ergibt sich:


Who is that strange amusing man?
Who is that [el]


Die print()-Anweisungen vor der continue-Anweisung werden ausgeführt, und dann die print()-Anweisung im finally-Block. Weil die continue-Anweisung bewirkt, daß die Ausführung oben in der while-Schleife fortgesetzt wird, entsteht eine Endlosschleife, und Sie müssen Strg+C drücken, um die Ausgabe abzubrechen. Für mysteriousState=4 ergibt sich:


Who is that strange but kindly amusing man?


Die print()-Anweisungen vor der throw-Anweisung werden ausgeführt, und dann die print()-Anweisung im finally-Block. Aufgrund der throw-Anweisung wird eine durch UncaughtException erzeugte Fehlermeldung auch ausgegeben. Für mysteriousState=5 (und jeden Integerwert, der nicht zwischen 1 und 4 liegt) ergibt sich:


Who is that strange but kindly not at all amusing man?
I'd like to meet the man. Who is that strange but kindly not ...


Die print()-Anweisungen vor dem finally-Block werden ausgeführt, und dann die print()-Anweisung im finally-Block. Weil die while-Schleife immer true ergibt, ist dies eine Endlosschleife, und Sie müssen Strg+C drücken, um die Ausgabe abzubrechen.

Das ist zwar ein eher künstliches Beispiel, aber es zeigt, wie der finally-Block verwendet werden kann, um Aufräumarbeiten auszuführen.

Einschränkungen

So gut, wie sich das alles anhört, ist es nicht etwas einschränkend? Angenommen, Sie wollten eine der Standardmethoden der Object-Klasse überschreiben, toString(), damit sie eine etwas bessere Ausgabe realisiert:

public class MyIllegalClass {
public String toString() {
someReallyExceptionalMethod();
. . . // gibt einen String zurück
}
}

Weil die Oberklasse Object die Methodendeklaration für toString() ohne throws-Klausel implementiert hat, muß auch jede Implemetierung einer Unterklasse dieser Einschränkung gehorchen. Mit anderen Worten, Sie können keine throws-Klausel in Ihrer Methodendefinition, die toString() überschreibt, verwenden, weil die Originaldefinition von toString() auch kein throws enthielt.

Insbesondere können Sie nicht einfach someReallyExceptionalMethod() in der überschreibenden Definition aufrufen, weil dadurch zahllose Fehler und Ausnahmen entstehen können, die zum Teil nicht davon befreit sind, in einer throws-Klausel aufgelistet zu werden (wie etwa IOException und MyFirstException). Wenn alle aufgeworfenen Ausnahmen befreit wären, hätten Sie kein Problem. Es ist jedoch wie es ist, und Sie können throws nicht verwenden, um sie weiterzugeben, und Sie müssen zumindest diese Ausnahmen auffangen, damit das folgende in java möglich ist:

public class MyLegalClass {
public String toString() {
try {
someReallyExceptionalMethod();
}
catch (IOException e) {
}
catch (MyFirstException m) {
}
. . . // gibt einen String zurück
}
}

In beiden Fällen fangen Sie Ausnahmen auf und tun überhaupt nichts mit ihnen. Das ist zwar erlaubt, aber nicht immer sinnvoll. Es ist vielleicht ein bißchen aufwendig, einen guten, nicht-trivialen catch-Block zu entwickeln. Die Methode toString() von MyIllegalClass würde aber einen Compiler-Fehler erzeugen, der Sie daran erinnert, eine Verarbeitung aufzunehmen. Der zusätzliche Aufwand wird Sie jedoch reich belohnen, wenn Sie Ihre Klassen in späteren Projekten wiederverwenden, in immer größer werdenden Klassen. Er macht Ihre Programme robuster, sie können ungewöhnliche Eingaben besser verarbeiten, und sie werden wahrscheinlich besser funktionieren, wenn sie mehrere Threads verwenden. Die Java-Klassenbibliothek wurde natürlich mit dieser Art Sorgfalt entwickelt, was auch einer der Gründe dafür ist, warum sie robust genug ist, in allen Ihren Projekten eingesetzt werden zu können.

Gebräuchliche Ausnahmen

Manchmal müssen Sie eigene Ausnahme-Klassen entwickeln. Das ist in Java ganz einfach. Sie erweitern einfach die Klasse Exception und definieren das Verhalten, das Ihre Ausnahme haben soll:

class MyFirstException extends Exception {
MyFirstException() {
super();
. . . // Fehlerverarbeitung
}
MyFirstException(String errMsg) {
super(errMsg);
. . . // Fehlerverarbeitung
}
}

In diesem Beispiel ist MyFirstException als Unterklasse von Exception definiert, und sie erbt alle ihre Attribute und Verhalten. Diese Ausnahme kann ohne Argumente aufgerufen werden, oder mit einem String-Argument. Rufen Sie den Konstruktor der Oberklasse auf und fügen Sie Ihren Code für die Fehlerverarbeitung ein. So einfach ist das.

Zusammenfassung

Heute haben Sie gelernt, wie Ausnahmen das Design und die Robustheit Ihres Programms verbessern. Dieses Wissen gibt Ihnen zusammen mit den Informationen aus Kapitel 8, wo Sie die Ausnahmen kennengelernt haben, ein solides Verständnis für den Umgang mit Ausnahmen.

Sie haben die Ausnahmen kennengelernt, die in der Java-Klassenbibliothek definiert sind und aufgeworfen werden, wie die throws-Klausel eingesetzt wird, wie eine neue Ausnahme aufgeworfen wird, und wie eine bereits verarbeitete Ausnahme erneut aufgeworfen wird. Sie wissen, wie ein try- und catch-Blöcke verwendet werden, ebenso wie der finally-Block.

Die strenge Ausnahmeverarbeitung in Java erlegt dem Programmierer einige Einschränkungen auf, aber Sie haben erfahren, daß diese Einschränkungen durch die zuverlässigen und robusteren Programme bei weitem aufgehoben werden. Und Sie haben erfahren, daß sie durch die Erweiterung der Exception-Klasse eigenen Hierarchien für die Fehlerbehandlung aufbauen können.

F&A

F Ich bin noch ein bißchen unsicher, was den Unterschied zwischen Error, Exception und RuntimeException betrifft. Könnten Sie mir das noch einmal erklären?

A Error-Fehler werden beim dynamischen Linken oder bei Problemen mit der VM erzeugt und sind damit auf einer zu niedrigeren Ebene, um von allen Programmen verarbeitet werden zu können (obwohl komplexe Entwicklungsbibliotheken und -Umgebungen vielleicht ganz gut dafür geeignet sind).

RuntimeException-Fehler werden bei der normalen Ausführung von Java-Code erzeugt und weisen in der Regel auf einen Programmierfehler hin. Sie müssen also einfach eine Fehlermeldung ausgeben, um diesen Mangel zu kennzeichnen.

Exception-Fehler, die keine RuntimeException-Fehler sind (z.B. IOException), sind Bedingungen, die von jedem robusten und wohl-durchdachten Programm explizit verarbeitet werden sollten. Die Java-Klassenbibliothek wurde unter Verwendung von nur einigen wenigen davon geschrieben, aber diese wenige sind extrem wichtig, damit das System sicher und korrekt verwendet werden kann. Der Compiler erinnert Sie daran, diese Ausnahmen zu verarbeiten, indem er throws-Klauseln überprüft.

F Gibt es eine Möglichkeit, die strengen Einschränkungen für Methoden zu umgehen, die die throws-Klausel einführt?

A Ja. Angenommen, Sie haben lange und sorgfältig überlegt und sind zu der Erkenntnis gekommen, daß Sie diese Einschränkung umgehen müssen. Das ist zwar so gut wie nie der Fall, weil die richtige Lösung wäre, noch einmal zurückzugehen und Ihre Methoden anders zu entwerfen. Angenommen aber, daß eine Systemklasse Sie aus irgendeinem Grund in ein festes Muster zwängt. Ihre erste Lösung wäre, eine Unterklasse von RuntimeException anzulegen, um eine neue Ausnahme anzulegen, die Sie aus der throws-Klausel ausklammern können. Jetzt können Sie nach Herzenslust throw verwenden, weil die throws-Klausel, die das verhinderte, diese neue Ausnahme nicht aufnehmen muß.

Wenn Sie sehr viele solcher Ausnahmen benötigen, wäre es ein eleganter Ansatz, sie in neuen Ausnahme-Schnittstellen zu kombinieren, die sie Ihren neuen RuntimeException-Klassen hinzufügen könnten. Sie können frei wählen, welche Untermenge dieser neuen Schnittstellen Sie auffangen wollen (weil sie Unterklassen von RuntimeException sind), und alle restlichen RuntimeException-Fehler können diese ansonsten müßige Standard-Methode in der Bibliothek durchlaufen.

F In dem Bewußtsein, wie müßig es manchmal sein kann, Ausnahmebedingungen korrekt zu verarbeiten, was hält mich davon ab, alle Methoden wie folgt zu schreiben und einfach alle Ausnahmen zu ignorieren?

try { anyAnnoyingMethod(); } catch (Throwable t) { }

A Nichts, außer Ihrem Verstand. In einigen Fällen sollten Sie wirklich nichts tun, weil es das einzig richtige für die Implementierung Ihrer Methode ist. Aber in allen anderen Fällen sollten Sie die Mühsal überwinden und Erfahrung sammeln. Guter Stil kann manchmal aufwendig sein, auch für die besten Programmierer, aber Sie werden sehen, es lohnt sich.

Workshop

Der Workshop bietet zwei Möglichkeiten, zu überprüfen, was Sie in diesem Kapitel gelernt haben. Der Quiz-Teil stellt Ihnen Fragen, die Ihnen helfen sollen, Ihr Verständnis für den vorgestellten Stoff zu vertiefen. Die Antworten auf die Fragen finden Sie in Anhang A. Der Übungen-Teil ermöglicht Ihnen, Erfahrungen in der Anwendung der Dinge zu sammeln, die Sie hier kennengelernt haben. Versuchen Sie, diese Dinge durchzuarbeiten, bevor Sie mit der nächsten Lektion weitermachen.

Quiz

Übung

Erzeugen Sie eine Hierarchie eigener Ausnahmen für das Projekt, das Sie in der Übung zu Kapitel 6 entwickelt haben.


© 1997 SAMS
Ein Imprint des Markt&Technik Buch- und Software- Verlag GmbH
Elektronische Fassung des Titels: JBuilder in 14 Tagen, ISBN: 3-87791-895-6

Previous Page Page Top TOC Index Next Page See Page