Kapitel 7 Ausnahmen
Methoden müssen auf Laufzeitfehler, die bei der Programmausführung auftreten, geeignet reagieren, entweder selbst oder indem sie die Ursache und die Kontrolle an die aufrufende Methode weiterreichen. Diesen Kontrollmechanismus nennt man in Java Ausnahmebehandlung.
Die Behandlung von Laufzeitfehlern ist in der Java-Plattform auf Basis einer differenzierten Hierarchie von Ausnahme- bzw. Exception-Klassen gelöst.
Der effiziente Einsatz von Ausnahmen zum Design robuster Klassen ist ergo das zentrale Thema.
Compiler-Fehler
vs. Laufzeitfehler
Fehler können anhand des Zeitpunkts ihres Auftretens unterschieden werden: Fehler treten schon bei der Übersetzung oder erst zur Laufzeit auf.
Sicherlich sind Fehler, die der Compiler entdeckt, die angenehmeren. Man kann sie analysieren und den Code entsprechend anpassen.
Im ungünstigeren Fall treten Laufzeitfehler erst beim Anwender – dem Kunden – auf und beenden das Programm ohne geeignete Behandlung, lassen es »abstürzen«.
Die Behandlung der Laufzeitfehler macht bei interaktiven Programmen häufig den Hauptteil des Codes aus. Die produktiven Programmteile sind mit unzähligen Kontrollanweisungen durchsetzt, die auf widrige Umweltsituationen reagieren müssen.
Laufzeitfehler werden häufig ignoriert!
Bei traditionellen Sprachen wie C besteht das Übel darin, dass Fehler, die an eine aufrufende Methode als Ergebnis oder Parameter weitergereicht werden, durchaus ignoriert werden können.
Dies führt dann zu dem noch unangenehmeren Phänomen, dass die Absturzursache unklar bleibt.
Ausnahmen können nicht ignoriert werden
In Java begegnet man diesem Laissez-faire-Verhalten mit der Erzeugung und dem Auslösen von Ausnahme-Objekten1 , wenn nötig, durch die JVM selbst.
Diese Ausnahme-Objekte, einmal auf den Weg gebracht, müssen im Programm abgefangen werden oder erzeugen nette Konsol-Meldungen.
7.1 Konzeption
Bei der Konzeption neuer Sprachen geht man davon aus, dass eine konventionelle Behandlung der meisten Laufzeitfehler nicht genügt.
Die wesentlichen Design-Anforderungen sind
Forderungen an die Behandlung von Laufzeitfehlern
|
minimal-invasiv: Eine möglichst geringe (negative) Auswirkung auf den normalen Programmablauf (normal flow of control). |
|
Klarheit: Konzentration der Fehlerbehandlung auf klar erkennbare Programmabschnitte. |
|
Erweiterbarkeit: Je nach Programmumgebung treten andere, neue Laufzeitfehler auf, die in das System integiert werden müssen. |
|
Unterscheidung von Laufzeitfehlern
Auch Laufzeitfehler können je nach Art oder Ursache in Kategorien unterteilt werden. Im Sprachverständnis von Java signalisieren Laufzeitfehler zwei unterschiedliche Arten von Ausnahme-Situationen:
Fehlerhafter Code
|
fehlerhaften Programm-Code, der verbessert werden muss |
Unpassende Stelle
|
Fehler, die dort, wo sie auftreten, nicht sinnvoll behandelt werden können |
Zum ersten Fall: Der Programm-Code muss so verbessert werden, dass diese Fehler nicht mehr auftreten können.
Zum zweiten Fall: Der normale Programmablauf muss zwangsläufig unterbrochen werden und der Fehler an einer »angemesseneren« Stelle behandelt werden. Eine Rückkehr an den Punkt des Auftretens wird ausgeschlossen.
Java-Konzeption:
Es gibt kein Zurück
(not recoverable)
Das Konzept von Java ist stringent und durchaus diskussionswürdig. Es steht im Gegensatz zu Konzepten, die auch Laufzeitfehler in den normalen Kontrollfluss einbinden können.2
Visual Basic – ich bitte um Nachsicht – unterscheidet u.a. leichte Fehler, die erst einmal ignoriert werden können (On Error Resume Next). Schwere Fehler müssen zwar sofort behandelt werden, aber erlauben es durchaus, anschließend wieder an der Stelle fortzufahren, an der sie aufgetreten sind.
7.2 Ausnahme-Mechanismus
Basisklasse
Throwable
In Java sollen Ausnahmen immer Instanzen von Subklassen der Klasse java.lang.Throwable sein. Bei einem Laufzeitfehler muss man das Auslösen einer Ausnahme vom Behandeln der Ausnahme unterscheiden.
7.2.1 Ausnahme-Auslösung (exception throwing)
Wer löst Ausnahmen aus?
Eine Ausnahme kann wie folgt ausgelöst werden:
|
durch die JVM |
|
explizit durch eine Anweisung |
Ausnahme-Behandler (exception handler)
Die Ausnahme ist immer eine Instanz einer Ausnahme-Klasse, die den entsprechenden Laufzeitfehler identifiziert und wird von der JVM an einen passenden Ausnahme-Behandler weitergereicht (siehe 7.2.2).
throw:
explizites Auslösen einer Ausnahme
Explizit wird eine Ausnahme durch
throw throwableExpression;
ausgelöst, wobei throwableExpression eine Instanz vom (Sub-)Typ Throwable ergeben muss.
Beispiele
In der folgenden Methode wird eine Ausnahme durch die JVM ausgelöst, wenn der Divisor Null ist:
Implizites Auslösen einer Ausnahme
static int intDiv (int i, int j) {
return i/j; // bei einer Ausnahme erfolgt kein return
}
Hat j den Wert 0, erzeugt die JVM eine Instanz der Klasse ArithmeticException und aktiviert die Ausnahme.
In der nächsten Methode wird bei einem null-Argument eine Instanz von NullPointerException erschaffen und mittels throw ausgelöst:
Explizites Auslösen einer Ausnahme
class Point { int x,y; /*...*/
}
static double distanceFromOrigin (Point p) {
if (p==null)
throw new java.lang.NullPointerException("p ist null");
return Math.sqrt(p.x*p.x+p.y*p.y);
}
Bei diesen beiden Methoden wird die return-Anweisung beim Auftreten einer Ausnahme nicht mehr ausgeführt.
7.2.2 Ausnahme-Behandlung (exception handling)
Die Behandlung einer Ausnahme erfolgt nach generellen Regeln:
Unbehandelte Ausnahme
1. |
Wird eine Ausnahme ausgelöst, gilt sie als unbehandelt und führt zum sofortigen Abbruch des normalen Programmablaufs. |
Such-Muster
der JVM
2. |
Die JVM sucht als Nächstes im umgebenden Code bzw. in den aufrufenden Methoden so lange nach einer try-Anweisung, bis sie einen passenden catch-Block findet oder aber die oberste Methode der Threads, main() oder run(), verlassen muss. |
Unbehandelte Ausnahme: Abbruch!
3. |
Das Verlassen des obersten Methoden-Blocks führt bei einer unbehandelten Ausnahme zum Abbruch (der Thread). |
try behandelt
Ausnahmen
4. |
Nur eine try-Anweisung kann Ausnahmen behandeln: |
try { tryStatementSequenz }
[ catch (ExceptionType1 e1) { exceptionHandlerSequenz1 }
...
catch (ExceptionTypeN eN) { exceptionHandlerSequenzN } ]
[ finally { finallyStatementSequenz } ]
Dem try-Block muss zumindest ein optionaler catch- oder finally-Block folgen.
catch-Behandlung von Ausnahmen
5. |
Sofern catch-Blöcke vorhanden sind, |
|
|
werden nacheinander die einzigen Parameter ExceptionTypeX darauf untersucht, ob die unbehandelte Ausnahme-Instanz ein (Sub-) Typ ist. |
|
|
wird bei einem passenden ExceptionTypeX der zugehörige catch-Block ausgeführt, alle nachfolgenden catch-Blöcke übersprungen und die Ausnahme gilt als behandelt. |
|
|
kann hierdurch erneut eine Ausnahme ausgelöst werden. |
finally ist
unabhängig von Ausnahmen
6. |
Sofern ein finally-Block vorhanden ist, |
|
|
wird er immer am Ende der try-Anweisung ausgeführt, egal, ob eine Ausnahme aufgetreten, behandelt oder nicht behandelt ist. |
Lost Exception!
|
|
kann eine unbehandelte Ausnahme durch eine return-Anweisung »verloren” gehen oder aber durch die Auslösung einer neuen Ausnahme ersetzt werden. |
Ausnahme
unbehandelt? Propagieren!
7. |
Wurde die Ausnahme behandelt und keine neue ausgelöst, wird die Ausführung nach der try-Anweisung fortgesetzt. Ansonsten wiederholt sich die Suche, beginnend mit dem zweiten Schritt. |
Ausnahmen können also nur mit Hilfe von catch-Ausnahme-Parametern erkannt und behandelt werden.
Der finally-Block kann nur indirekt Ausnahmen durch Überschreiben oder Missachtung entfernen, was durchaus zu Problemen führen kann (siehe hierzu 7.3.3).
Aktivitäts-Diagramm (UML)
Aktivitäts-Diagramme:
Darstellung von Prozessen in UML
Aktivitäts-Diagramme wurden in Kapitel 4, Modellierung und UML, zwar nicht formal vorgestellt, sind aber zur Darstellung von Prozessabläufen und Bearbeitungsschritten recht reizvoll.
Start und Ende des Ablaufs sind unschwer an den massiven Kreisen zu erkennen. Die Bearbeitung (Aktivität) wird in Rechtecken dargestellt und folgt den Pfeilen und Entscheidungsrauten, wobei die jeweilige Bedingung (guard) in eckigen Klammern erfüllt sein muss.
Objekte, die beteiligt sind, und Kommentare runden das Diagramm ab.
Aktivitäts-Diagramm
zur Behandlung von Ausnahmen
Abbildung 7.1 Behandlung von Ausnahmen vom Typ Exception
In Abb. 7.1 wird der Versuch unternommen, das generelle Muster der Ausnahmebehandlung – analog zu den genannten Bearbeitungsschritten – anhand eines Aktivitäts-Diagramms darzustellen.
Beispiele
Auslösen einer NullPointerException
Zuerst wird in der Methode distanceFromOrigin() (siehe 7.2.1) eine NullPointerException ausgelöst:
double d= 0.0;
try {
d= distanceFromOrigin(null);
System.out.println(d); // wird nicht ausgeführt ¨
}
catch (Exception e) { d= Double.NaN; }
finally { System.out.println(d); } // :: NaN
Exception-Handling
2. Schritt
Erklärung: Die Ausnahme wird in distanceFromOrigin() ausgelöst und nicht behandelt. Also sucht die JVM im umgebenden Block.
4. Schritt
Die Suche endet in der try-Anweisung, die die Methode enthält.
1. Schritt
Diese wird sofort abgebrochen, ohne println() in Zeile ¨ auszuführen.
5. Schritt
Da NullPointerException eine Subklasse von Exception ist, ist der einzige catch-Block der passende Ausnahme-Behandler und die Ausnahme gilt als behandelt.
6. Schritt
Abschließend wird noch der finally-Block ausgeführt.
Variationen:
catch-Blöcke, finally-Block
Die folgende Klasse Test ist aufgrund ihrer verschiedenen Versionen interessant:
public class Test {
public static void main(String[] args) {
int[] iarr= {0,1,2};
int i=0;
/* Version 1 */
try { while(true) System.out.println(1/iarr[i++]); }
catch (IndexOutOfBoundsException e) {
System.out.println("Ind");
}
/* Version 2
catch (ArithmeticException e) {
System.out.println("Ari");
} */
/* Version 3
finally { System.out.println("fin");
} */
}
}
Exception-Handling
2. Schritt
Version 1 (nur erster catch-Block): In der while-Schleife tritt sofort eine ArithmeticException auf. Da ArithmeticException keine Subklasse von IndexOutOfBoundsException ist, passt der erste catch-Block nicht. Die JVM muss die Applikation Test abbrechen.
5. Schritt
Version 2 (inkl. zweitem catch-Block): Nun gibt es einen passenden catch-Block. Die Ausnahme gilt als behandelt und das Programm beendet normal (:: Ari).
6. Schritt
Version 3 (nur erster catch-Block und finally): Es gibt zwar keinen passenden catch-Block, allerdings wird vor dem Programmabbruch erst finally ausgeführt (:: fin).
7.3 Details zur Ausnahme-Behandlung
Es gibt noch einige interessante Details, die in den Bearbeitungsschritten (siehe 7.2.2) noch nicht angesprochen wurden.
7.3.1 catch-Reihenfolge
Folgen einem try-Block mehrere catch-Blöcke, muss aufgrund der sequenziellen Abarbeitung der catch-Blöcke folgende Reihenfolge eingehalten werden:
Reihenfolge der
catch-Exceptions
|
catch-Blöcke mit spezielleren Ausnahmen müssen vor catch-Blöcken mit generellen Ausnahmen angegeben werden. |
Der Compiler prüft die Einhaltung der Regel und erzeugt ggf. eine »unreachable«-Fehlermeldung:
Nach dem fünften Schritt in 7.2.2 verdeckt ein generellerer Ausnahmetyp alle speziellen, d.h. alle nachfolgenden Subtypen, die somit nicht ausgeführt werden könnten.
Das kleine Beispiel »Test« provoziert eine Fehlermeldung:
public class Test {
public static void main(String[] args) {
Compiler erzeugt »unreachable« -Meldung
try { System.out.println(1/0); }
catch (Exception e) { } // C-Fehler
catch (ArithmeticException e) { }
}
}
Ein Vertauschen der beiden catch-Blöcke löst das Problem.
7.3.2 Geschachtelte Ausnahmen
try-Anweisungen schachteln
Ausnahme-Behandlungen können ineinander geschachtelt werden, was sehr angenehm sein kann.
try-block
try-block
try-block
...
catch-blocks
finally-block
catch-blocks
finally-block
Ausführung von geschachtelten try-Anweisungen
Gemäß dem fünften Schritt in 7.2.2 werden bei Auslösen einer inneren Ausnahme zuerst alle zugehörigen inneren catch-Blöcke durchsucht. Sollte es keinen passenden catch-Block geben, erfolgt nach dem zweiten Schritt sukzessive die Suche nach einer try-Anweisung in den umgebenden Blöcken.
Beispiel
static int div (Integer i1, Integer i2) {
try {
try {
return i1.intValue()/i2.intValue();
}
catch (NullPointerException npe) {
return 0;
}
}
catch (ArithmeticException ae) {
return Integer.MAX_VALUE;
}
/* finally { System.out.print("fin "); } */
}
Version ohne finally-Block:
Integer i= new Integer(7);
System.out.println(div(i,null)); // :: 0
System.out.println(div(i,new Integer(0))); // ::
2147483647
System.out.println(div(i,new Integer(2))); // ::
3
Version mit finally-Block : Vor jeder Zahl erfolgt die Ausgabe fin.
7.3.3 finally und unbehandelte Ausnahmen
Funktion von finally in der
try-Anweisung
In einem finally-Block hat man keinen direkten Zugriff auf die ausgelöste Ausnahme. Allerdings kann man – meist aus Versehen – die Ausnahme durchaus entfernen oder mit einer anderen überschreiben. Wichtig ist die folgende Regel:
Unbehandelte Ausnahme
|
Es kann nur jeweils eine unbehandelte Ausnahme von einer Methode bzw. einem Block nach außen übergeben werden. |
Aufgrund dieser Regel wird eine Ausnahme, die im try-Block nicht behandelt wird, durch eine Ausnahme überschrieben, die im finally-Block ausgelöst und nicht behandelt wird.
Beispiele
finally-Problem:
verlorene Ausnahme!
Die Methode finallyTest1() erzeugt eine verlorene Ausnahme:
static void finallyTest1() throws Exception {
try { throw new Exception("try"); }
finally { throw new Exception("fin"); }
}
Der Aufruf erzeugt nach der o.a. Regel:
try {
finallyTest1();
}
catch (Exception e) {
System.out.println(e.toString()); // :: java.lang.Exception:
fin
}
Die Variation finallyTest2() beseitigt das Problem der verlorenen Ausnahme:
finally-Variation mit innerem
try-catch
static void finallyTest2() throws Exception {
try { throw new Exception("try"); }
finally {
try { throw new Exception("fin"); }
catch (Exception e) {
/* wird hier behandelt */ }
}
}
return-Anweisung im finally-Block
In der folgenden Methode neverThrowsExceptions() hat die return-Anweisung in finally einen recht subtilen Nebeneffekt, der im zweiten Punkt des sechsten Schritts in 7.2.2 angesprochen wurde.
Wirkung von return im finally-Block
Es ist das Problem der verlorenen unbehandelten Ausnahme:
static void neverThrowsExceptions() throws Exception
{
try { throw new Exception(); } // geht verloren!
finally {
return; // Methode wird normal verlassen!
} // die Exception wurde implizit behandelt!
}
|
Eine Methode, deren finally-Block eine return-Anweisung ausführt, liefert keine Ausnahmen nach außen. |
7.3.4 Rethrowing in catch
Auch in catch-Blöcken können wieder neue Ausnahmen ausgelöst werden.
Rethrowing: Erneutes Auslösen derselben Ausnahme
|
Unter Rethrowing versteht man die erneute Aktivierung derselben Ausnahme mittels throw, die dem catch-Block übergeben wurde. |
Dies ist in Fällen sinnvoll, in denen catch – eventuell abhängig von bestimmten Bedingungen – die Ausnahme nur teilweise oder gar nicht behandeln kann und die Ausnahme deshalb weiterreichen muss.
Beispiel
In der Methode mean() wird eine Division durch 0 abgefangen (d.h., das Array hat keine Elemente). Wird mean() dagegen mit null aufgerufen, wird die Ausnahme nach außen weitergereicht.
Rethrowing einer RuntimeException
public static int mean(int[] iarr) {
try {
int m= 0;
for (int i=0; i<iarr.length; m+=iarr[i++]);
return m/iarr.length;
}
catch (RuntimeException e) {
if (e instanceof NullPointerException) throw e;
else return 0;
}
}
Hier drei verschiedene Aufrufe:
System.out.println(mean(new int[] {1,2,3}));
// :: 2
System.out.println(mean(new int[] {})); //
:: 0
System.out.println(mean(null)); // NullPointerException
7.4 Ausnahme-Kategorien in Java
Das Basis-Package java.lang stellt bereits eine umfangreiche Klassen-Hierarchie von Ausnahmen bereit, die Auswirkungen auf den Compiler und die Art der Verwendung hat.
7.4.1 Hierarchie-Konzept
Throwable stellt die Basisklasse aller Ausnahmen dar (Abb. 7.2). Nach Java-Konvention sollen
Erzeugung von Ausnahmen
|
von der Klasse Throwable – obwohl nicht abstrakt – keine eigenen Instanzen erzeugt werden und |
|
möglichst nur Ausnahmen von Leaf-Klassen3 erzeugt werden. |
Begründet wird der letzte Punkt damit, dass die Hierarchie eine disjunkte Klassifikation der Laufzeitfehler darstellt, die den Fehler immer weiter präzisiert. Eine Leaf-Klasse bzw. -Instanz ist also informativer.
Ausnahme-Hierarchie: eine disjunkte Ausnahme-Kassifikation
Abbildung 7.2 Ausnahme-Kategorien
Nach Throwable folgen zwei große Kategorien von Fehlern:
Ausnahme-
Hierarchie
|
Exception ist die Basisklasse für alle Ausnahmen, die im Code abgefangen und behandelt werden sollen. |
|
Error ist die Basisklasse für alle Fehler, die als nicht behebbar angesehen werden, also auch im Code nicht abgefangen werden sollen. |
Mit anderen Worten: Error-Ausnahmen sollen tabu sein.
7.4.2 Checked vs. unchecked Exceptions
checked vs. unchecked
Exceptions
Die Begriffe checked bzw. unchecked Exceptions stehen für Ausnahmen, die im Code geprüft oder deklariert bzw. nicht geprüft werden müssen.
Für Ausnahme-Klassen, die zu den checked Exceptions zählen, gilt folgende Regel:
Der Compiler stellt sicher, dass checked Exceptions
Compiler-Prüfung von checked Exceptions
|
nur dann in Methoden nicht behandelt werden brauchen, wenn sie im Methoden-Kopf mittels throws deklariert sind: |
[mModifiers] ResultType methodName (parameterList)
throws Throwable1[,...ThrowableN]
Warnung zu
unchecked Exceptions
|
in einer übergeordneten Methode entweder mittels try-catch abgefangen oder erneut im Kopf mittels throws deklariert werden. |
Für unchecked Exceptions gibt es keine Regel, nur einen Warnhinweis:
|
Diese Art von Ausnahmen kann zu jedem Zeitpunkt von der JVM oder explizit durch Code ausgelöst werden und führt dann ohne Behandlung immer zum Abbruch der Ausführung (Thread). |
Separation: checked vs. unchecked
Error/RuntimeException sind unchecked
Zu den unchecked Exceptions gehören alle Error- und RuntimeException-Klassen bzw. -Subklassen, zu den checked Exceptions dann die restlichen (siehe Abb. 7.2).
|
Der Vorteil des Konzepts von checked Exceptions liegt in ihrer expliziten Natur. Man erkennt sie sofort bei Verwendung und hat nur die Alternative zwischen Behandeln und erneutem Weiterreichen.
Compiler-Prüfung
checked bzw. unchecked
Exception: Compiler vs. JVM
Die Unterscheidung der Ausnahme-Klassen in checked bzw. unchecked ist »magic«, d.h. kann nicht durch ein Marker-Interface erfolgen.4 Eo ipso wird – man ahnt es bereits – die Einhaltung der Checked-Exception-Regel nur vom Compiler geprüft.
|
Die JVM kennt keine Unterschiede zwischen checked und unchecked Exceptions. |
7.4.3 Basisklasse Throwable
Throwable:
nur logisch abstrakt
Ein Blick in die Implementation von Throwable zeigt – im Gegensatz zu dem suggestiven Interface-Namen – eine voll implementierte Klasse, die allen Subklassen wie Exception oder Error praktisch bereits die Arbeit abnimmt.
Die wohl wichtigste Eigenschaft von Throwable, und damit aller Subklassen; ist die Fähigkeit, bei der Instanzierung einer Ausnahme eine Fehlerbeschreibung anzugeben.
Java bietet dazu – sofern erwünscht – einen ausführlichen Execution Stack Trace, d.h. eine Lokalisierung des Ursprungs und seiner Proliferation durch den ausgeführten Code.5
Da Throwable nicht selbst instanziert werden soll, erübrigt sich die Darstellung von Konstruktoren bzw. Methoden.
Error – »tödlicher« Laufzeitfehler
7.4.4 Ausnahmen vom Typ Error
Wie bereits in 7.4.1 erwähnt, signalisieren Fehler vom Typ Error: »Hände weg vom Programm«. Interessant ist nur, welche Fehler die Java-Entwickler darunter verstehen.
Die größte Gruppe, Ausnahmen vom Typ LinkageError, können zwar nicht im Programm-Code behandelt werden, müssen aber darauf untersucht werden, warum eine Klasse zur Laufzeit nicht eingebunden werden kann. Die häufigste Ausnahme NoClassDefFoundError kennt dagegen jeder Java-Programmierer.6
Ausnahmen vom Typ VirtualMachineError drücken Fehler innerhalb der JVM aus, wobei OutOfMemoryError und StackOverflowError »von außen« beeinflusst werden können.
Die Klasse ThreadDeath gehört zwar logisch nicht zum Typ Error, wurde hier aber platziert, um nicht mit einem catch(Exception e) versehentlich abgefangen zu werden.
Diese Art der Ausnahme wird durch den mutwilligen Thread-Tod mittels stop() ausgelöst und erzeugt – im Gegensatz zu anderen – keine Meldung. Man kann ihn also schlichtweg ignorieren.
7.4.5 Ausnahmen vom Typ Exception
Java-Konvention zum Typ Exception
Alle für den Code interessanten Ausnahmen sind vom Typ Exception, da sie entweder im Programm behandelt werden müssen oder fehlerhaften Code signalisieren, der verbessert werden muss.
Nach Java-Konvention
Deklaration und Benennung
|
enden alle Namen von Ausnahmen dieses Typs mit Exception. |
|
werden eigene Ausnahme-Klassen als Subklassen von Exception deklariert, und zwar normalerweise als checked Exception. |
7.4.6 Ausnahmen vom Typ RuntimeException
RuntimeException
signalisiert einen Programmierfehler
Ausnahmen vom Typ RuntimeException signalisieren, einfach gesagt, Fehler im Code, die der Compiler leider nicht aufdecken konnte.
|
Robuster Code muss so geschrieben werden, das Ausnahmen vom Typ RuntimeException nicht mehr auftreten. |
So einfach diese Aussage, so schwierig ihre Umsetzung. Denn diese Fehler
|
müssen nicht explizit abgefangen werden. |
|
werden häufig durch unzulässige Argumente beim Methodenaufruf ausgelöst. |
Der zweite Punkt fällt dann unter das Thema Kontraktmanagement.7
Gerade deshalb werden in der Tabelle 6.1 zu den häufigsten RuntimeExceptions exemplarisch einfache Vermeidungsstrategien angegeben.
Vermeidung von RuntimeExceptions
Tabelle 7.1 Vermeidungsstrategien zu RuntimeExceptions
RuntimeException |
Wird verhindert durch: |
ArithmeticException |
Vor div- bzw. mod-Operationen int-Operanden vorher prüfen |
IndexOutOfBoundsException |
String- oder Array-Index prüfen |
ClassCastException |
Mit instanceof-Operator den Typ prüfen |
NullPointerException |
Referenz auf nicht null prüfen |
NumberFormatException |
Konvertierungen von Strings nach numerische Typen in try-catch einbetten |
7.4.7 Checked Exceptions
Aufgrund des Lazy-Programmer-Syndroms fallen die meisten Ausnahmen in den Packages der Java-Plattform und anderer Anbieter unter die Kategorie »checked Exceptions«.
Checked Exceptions: unvermeidliche Umgebungsfehler
Im Gegensatz zu RuntimeException enthält diese Kategorie Fehler, die durch die Programmumgebung und nicht durch eigenen fehlerhaften Code verursacht werden.
Hierunter fallen u.a. Klassen, die nicht (mehr) gefunden werden, Ein- und Ausgabefehler, Kommunikations-, Netzwerk- oder andere Hardwareprobleme.
Insbesondere diese Fehler müssen in einem robusten Programm unbedingt abgefangen werden, und es müssen – je nach Aufgabe – Strategien implementiert werden, wie diesen Fehlern zu begegnen ist.8
7.5 Overriding von Ausnahmen
Beim Design von Methoden in einer Basisklasse bzw. einem Interface muss berücksichtigt werden, ob ein Überschreiben dieser Methode eventuell eine checked Exceptions notwendig macht. Denn es gilt die Regel:
Overriding bzw. Überschreiben von Ausnahmen
|
Overriding einer Methode, die eine checked Exception deklariert, kann nur durch eine Methode erfolgen, die alternativ |
|
|
Ausnahmen vom selben Typ oder Subtyp oder |
|
|
keine Ausnahmen |
deklariert.9
Erklärung: Diese »einengende« Vererbungsrestriktion musste für Klassen und Interfaces eingeführt werden. Der Grund liegt in der Kombination aus Substitutions- und Checked-Exception-Regel:
Denn ohne die Overriding-Regel könnten Instanzen einer Subklasse eine Ausnahme auslösen, die in der Superklasse nicht deklariert ist.
Eingesetzt in Code, der nur Ausnahmen der Superklasse abfangen muss, würden sie dann locker die Regel für checked Exceptions (siehe 7.4.2) aushebeln.
Beispiele
Overriding von Ausnahmen bei Vererbung
interface I { void f() /* throws Exception */ ;
}
class A { void f() throws Exception{} }
// C-Fehler, da f() in I keine Exception deklariert
class B extends A implements I {
public void f() throws Exception {}
}
Wird im Interface I die Methode f() analog zu A deklariert, gibt es aufgrund der Regel keinen Compiler-Fehler in Klasse B.
In den nächsten zwei Beispielen wird die Ausnahme-Hierarchie des Packages java.io verwendet (siehe Abb. 7.3).
Abbildung 7.3 Ausschnitt aus der Exception-Hierarchie von java.io
interface I { void f() throws FileNotFoundException;
}
class A { void f() throws EOFException{} }
class B extends A implements I {
// die nachfolgenden Deklarationen sind nicht
möglich
public void f() throws FileNotFoundException{} // C-Fehler
public void f() throws EOFException{} // C-Fehler
public void f() throws FileNotFoundException,
EOFException {} // C-Fehler
// einer der nachfolgenden Deklarationen ist möglich
public void f() {} // ok!
public void f() throws Error {} // auch ok!
}
Siehe hierzu die o.a. Regeln.
Implementierungs-Hierarchie
Auch bei Hierarchien gilt die Overriding-Regel für Ausnahmen, wie das nächste Beispiel zeigt:
interface I1 { void f() throws IOException;
}
interface I2 extends I1 {
void f() throws ObjectStreamException;
}
class B implements I2 {
// nur noch (Sub-)Typen von ObjectStreamException möglich
public void f() throws InvalidClassException {}
}
Die Klasse B muss sich an Interface I2 orientieren.
Abschließend die Konsequenz, formuliert als Design-Regel:
Notwendige Ausnahmen in Basisklassen
|
Methoden in Basisklassen bzw. Interfaces müssen bereits die checked Exceptions deklarieren, die in Subklassen benötigt werden. |
|
7.6 Deklaration neuer Ausnahmen
Die Standard-Plattform enthält eine unüberschaubare Zahl von speziellen Ausnahme-Klassen, auf die man – sofern sie passen – zurückgreifen kann.
Natürlich kann man in einem eigenen Package – analog zu java.io – auch eigene Ausnahme-Klassen deklarieren. Ob man eine checked oder unchecked Exception deklariert, soll erst im folgenden Abschnitt behandelt werden. Zuerst interessiert der Mechanismus.
Muster für die Deklaration neuer Ausnahmen
class NewException extends ExistingException
{
public NewException () {
// evtl. Aufruf von super(...);
}
public NewException (String description) {
super(description); // explizite Fehler-Beschreibung
}
}
Ist ExistingException eine unchecked Exception, ist dies NewException ebenfalls.
Mit super(description) wird eine Fehlerbeschreibung bis nach Throwable durchgereicht, der einen entsprechenden Konstruktor bereitstellt:
public class Throwable implements java.io.Serializable {
public Throwable(String message) {
fillInStackTrace(); detailMessage = message;
}
//...
}
Aufgrund des Marker-Interfaces Serializable können alle Ausnahmen gespeichert werden.10
Beispiel (Umwandlung einer Exception)10
Deklaration neuer Ausnahmen
Eine gültige Teile-Nummer besteht aus maximal zehn Stellen und enthält nur Ziffern oder Bindestriche.11 Es soll eine PrimaryKeyException im Konstruktor ausgelöst werden, wenn die Teile-Nummer ungültig ist. Diese soll explizit abgefangen werden.
class PrimaryKeyException extends Exception {
// checked!
public PrimaryKeyException () {
super("Fehlerhafter Teile-Primärschlüssel!");
}
public PrimaryKeyException (String description) {
super(description);
}
}
class Teil {
String nr;
Teil(String nr) throws PrimaryKeyException {
if (nr.length()>10) throw new PrimaryKeyException();
try { Long.parseLong(nr.replace('-','0')); }
catch (NumberFormatException e) { // Umwandlung einer
throw new PrimaryKeyException(); // Runtime-Exception
}
this.nr=nr;
}
//...
}
7.7 Einsatz von Ausnahmen
Bisher wurden Ausnahme-Mechanismen anhand bestehender Hierarchien vorgestellt und gezeigt, wie man korrekt reagiert.
Aber die eigentlich schwierig Frage, wie Ausnahmen in eigenen Applikationen sinnvoll und effizient eingesetzt werden, ist noch unbeantwortet.
Soviel vorab: Es gibt keine Ausnahme-Pattern, jedoch durchaus Idiome. Die Ausnahme-Behandlung ist mit anderen Worten an die jeweilige Sprache gebunden, und Antworten auf Fragen zum optimalen Ausnahme-Design bewegen sich gerade in Java auf dünnem Eis.12
7.7.1 Missbrauch von Ausnahmen
Zum Einstieg ein Hinweis:
Ausnahmen als
Ersatz für andere Kontrollstrukturen?
|
Ausnahmen dienen nicht als Ersatz für andere Kontrollstrukturen. |
Das folgende Code-Segment ist ein extremes Beispiel:
double[] val= {1.,2.,3.};
double sum=0.;
try {
int i=0;
while (true)
sum+= val[i++];
}
catch(Exception e) {} // ignoriert Index-Überschreitung!
System.out.println(sum); // :: 6.0
Natürlich sind die Dinge nicht immer so klar wie in diesem Beispiel.
7.7.2 Ausnahmen zur Einhaltung von Kontrakten
Assertion – Kontrakt
Unter Kontrakten – auch Assertions genannt – versteht man formale Eigenschaften, die vom Client einer Methode eingehalten werden müssen oder auch von der Methode selbst (siehe auch 6.12.1).
Eine häufige Kontraktverletzung besteht darin, die Preconditions (Vorbedingungen) beim Aufruf einer Methode nicht einzuhalten. Deshalb:
Ausnahmen: Einsatz bei
Kontrakt- Missachtung
|
Robuste Methoden erzwingen die Einhaltung ihrer Preconditions mit Hilfe von Ausnahmen. |
Diese Regel ist nicht ganz so trivial in der Umsetzung, denn es gibt Alternativen.
Default-Werte
bei Kontraktverletzung
Default-Alternative: Werden Vorbedingungen nicht eingehalten, kann man mit Hilfe von Default-Werten die Operationen durchführen. Dies muss für den Client allerdings klar erkennbar sein, da er ansonsten von den Ergebnissen sehr überrascht sein könnte.
Ausnahme-Auswahl: Zur Wahl stehen checked vs. unchecked Exceptions. Beim Typ RuntimeException ist dem Client freigestellt, ob er mittels try-catch fehlerhafte Argument abfängt oder ob er darauf verzichten will. Im anderen Fall lässt man ihm keine Wahl (siehe 7.7.3).
Kontrakt-Beispiel: Konstruktor
Ausnahmen im Konstruktor
Fehlerhafte Argumente bei Konstruktoren durch Default-Werte zu ersetzen, ist nicht gerade genial. Wozu ist wohl ein No-Arg-Konstruktor da?
Ohne die Hilfe von Ausnahmen kann sich ein Konstruktor nicht wirksam gegen fehlerhafte Aufrufe wehren, da konventionelle Fehlermitteilungen durch Rückgabewerte entfallen.
Ausnahme verhindert Instanz-Anlage
|
Bei Auslösen einer Ausnahme in einem Konstruktor wird die Instanz nicht angelegt, und eine Referenz auf die Instanz hat weiterhin den alten Wert.13 14 |
Beispiel14
class Teil {
static final String mes= "Fehlerhafter Teile-Schlüssel!";
String nr;
// throws nicht notwendig, aber eine explizite
Drohung!
Teil(String nr) throws IllegalArgumentException {
if (nr.length()>10) throw new IllegalArgumentException(mes);
try { Long.parseLong(nr.replace('-','0')); }
catch (NumberFormatException e) {
throw new IllegalArgumentException(mes);
}
this.nr=nr;
}
}
Der Vorteil einer Unchecked-Variante liegt beim Client, der nun die Wahl hat:
public class Test {
public static void main(String[] args) {
String nr="234-x";
Teil t= null;
try { t= new Teil(nr); }
catch (RuntimeException re) {
System.out.println(t==null); // :: true
}
// ohne try Programm-Abbruch mit Ausnahme
new Teil(nr);
}
}
7.7.3 Kommunikation per Ausnahme
Die JVM unterscheidet überhaupt keine Ausnahmen, der Java-Compiler kennt nur checked und unchecked Exceptions, eine hilfreiche, aber viel zu grobe Unterteilung.
Ausnahmen
differenziert nach Kommunikationsart
Abbildung 7.4 Ausnahme-Klassifikation anhand der Kommunikationsart
Die Kommunikation zwischen Sender (Server) und Empfänger (Client) mittels Ausnahmen sollte man ein wenig genauer differenzieren (siehe Abb. 7.4).
Kommunikationssignal
Kommunikations-signal
Ausnahmen, die zu einer Unterbrechung des normalen Programmablaufs führen, können auch Kommunikations-Signale sein:
Ausnahme als Interrupt-Objekt
|
Begreift man eine Ausnahme als besonderes Signal15 des Senders, dann ist es ein Interrupt-Objekt, welches den normalen Programmablauf eines eventuell unbekannten Empfängers unterbricht. |
Spätestens bei den Threads wird klar, das Ausnahmen als Signale auch zu Kommunikationsaufgaben herangezogen werden, was aber leider nur sehr ungenügend von der Java-Sprache unterstützt wird.
Fehler im Code
Ausnahme:
Signalisieren
von Code- oder Umgebungsfehler
Fehler-Signale können – der Interpretation der Java-Entwickler folgend – in Fehler im Code und Umgebungsfehler unterschieden werden
Fehler im Code müssen letztendlich entfernt werden (siehe 7.4.6).
Hat man für diese Art von Laufzeitfehlern checked Exceptions gewählt, steht man anschließend vor vielen try-catch-Anweisungen, die den Code sinnlos aufblähen. Dies führt zum ersten Separations-Idiom.
RuntimeException: geeignet für Fehler im Code
Exception-Separation: Unterscheide Fehler im Code von Umgebungsfehlern und signalisiere Codierungsfehler mit Hilfe einer Subklasse von RuntimeException. |
Die Nichteinhaltung von Kontrakten fällt häufiger unter die Rubrik »Fehler im Code«. Ein unzulässiges null-Argument kann durchaus mit einer NullPointerException geahndet werden. Ein Client hat ja die Wahl, den Kontrakt einzuhalten.
Umgebungsfehler
RuntimeException: ungeeignet für Umgebungsfehler
Umgebungsfehler dürfen nicht ignoriert werden. Hierfür ist eine anonyme RuntimeException nicht sinnvoll, da man bei der Benutzung des Services die try-catch-Anweisungen nicht weglassen darf.
Normale Reaktion: Die Server-Methode erklärt die Ausnahme im Kopf der Methode mittels throws als Subtyp von Exception (nicht Runtime!) und erwartet, dass sich der Client um das Problem kümmert.
Error: tötlicher Umgebungsfehler
Harte Reaktion: Die Server-Methode löst eine Ausnahme vom Typ Error aus. Als Resultat stirbt wahrscheinlich auch der Client, da dieser mit catch(Exception e) {...} keinen Error abfangen kann.
Fatale vs. nicht fatale Fehler
Fatale Fehler:
der Client
entscheidet
Der Client unterscheidet die bei ihm eintreffenden Ausnahmen in fatale Fehler, die ihn seinerseits zum Abbruch zwingen, und in nicht fatale.
|
Was fatal ist, bestimmt der Client. |
Nicht fatale Fehler:
Wiederholen/Ignorieren
|
Nicht fatale Fehler können mit der Strategie »Wiederholen« (mit/ohne Variation) oder »Ignorieren« behandelt werden. |
Ein gescheiterter Verbindungsaufbau oder Sperrversuch eines Datensatzes führt zwar zu einer Ausnahme, aber eventuell erst nach mehrmaliger Wiederholung zu einem fatalen Fehler.
In einer Multimedia-Anwendung mag es Laufzeitfehler geben, die aber bis zu einem Grenzwert ignoriert werden müssen.16 Dies führt zum zweiten Separations-Idiom.
Separiere fatale Fehler
Exception-Separation: Der Client unterscheidet Ausnahmen nach fatalen und überbrückbaren Fehlern. |
7.8 Ausnahmen-Behandlung
Ausnahmen zu deklarieren und auszulösen ist die eine Sache, die richtige Behandlung der Ausnahmen im Client-Code meistens die schwerere.
Ein Grund liegt darin, dass try-catch-Anweisungen logisch ähnlich aufgebaut sind wie switch- und goto-Anweisungen.
Ist dann der Code, der auf try-catch-Anweisungen folgt, auch noch abhängig davon, ob und welche Ausnahmen aufgetreten sind, führt dies zum Spaghetti-Syndrom.17
7.8.1 Beispiel: Messwert-Import
Messwert-Import
Ausnahme-Behandlung an kurzen Code-Fragmenten zu diskutieren, hat wenig Überzeugungskraft. Deshalb sollen anhand des Beispiels »Messwert-Import« die bisherigen und folgenden Design-Vorschläge diskutiert und umgesetzt werden.
Aufgabe: Einlesen und Prüfen
Aufgabe: Einlesen und Überprüfen von Messwert-Paaren (Datum, Temperatur) aus einer Textdatei, Übertragen der Werte in eine Datenbank und Bereitstellen der gelesenen Werte in einem Container (siehe Abb. 7.5).
Bildliche Vorstellung der Aufgabe
Abbildung 7.5 Messwert-Import in eine RDBMS-Tabelle
Wichtig: Bei der Klasse TempImport steht primär das Ausnahme-Design im Mittelpunkt. Das Klassen-Modell und die RDBMS/JDBC-Anweisungen interessieren hier nur marginal.
Klassen zu
Messwert-Import: TempValue TempImport
Abbildung 7.6 Applikation TempImport
Messwert-Import-Design: Ausnahme-Sicht
Die Entscheidung über die Übertragung der neuen Messwerte in die Datenbank kann aufgrund der Restriktionen erst am Ende des Einlesens fallen. Da die Messwerte als Objekte in eine Kollektion übertragen werden, ist eine minimale Messwert-Klasse TempValue nützlich.
Betrachten wir die beiden Klassen nur aus der Sicht der Ausnahmen.
Ausnahmen zur Klasse TempValue
class TempValue {
public TempValue(String sDate, String sTemp)
throws ParseException, NumberFormatException,
RangeException /* <-zu deklarieren */ {}
}
Server-Sicht: Die Ausnahmen des Konstruktors zählen natürlich zu den fatalen Fehlern (in TempValue); die Instanz wird nicht angelegt.18 Zumindest zwei der drei Ausnahmen sind notwendig, um Formatfehler von der Überschreitung des erlaubten Temperatur-Intervalls zu unterscheiden.
Client-Sicht: Aus der Sicht der Client-Klasse TempImport sind alle drei Ausnahmen nicht fatal, da fehlerhafte Messwert-Zeilen erst einmal nur registriert werden und nicht zum Abbruch führen.
Ausnahmen
zur Klasse TempImport
Die zweite Klasse hält eine kleine Überraschung bereit:
class TempImport {
public static void importTemp(String fileName,
Collection tempValues)
throws ClassNotFoundException, FileNotFoundException,
ParseException, RangeException, SQLException,
IOException, NullPointerException {}
}
Ein Client der Methode importTemp() wird mit einer wahren Flut von Ausnahmen aus unterschiedlichen Packages konfrontiert.
Wir reagieren mit einer Design-Regel:
Keine Proliferation von Ausnahmen diverser Packages
|
Man kann den Client nicht mit allen möglichen fatalen Ausnahmen aus verschiedenen Packages konfrontieren, die man selbst nutzt. |
|
Spontane Folgerung: Eine Methode erklärt nur noch die Ausnahme Exception!
Wirkung: Der Client kann entweder seinerseits mit einer schönen Konsol-Meldung abbrechen oder mit Hilfe des instanceof-Operators versuchen, auf die einzelnen Fehler sinnvoll zu reagieren.
Der Operator instanceof ersetzt aber nur catch-Blöcke durch if-Anweisungen und setzt wiederum voraus, dass die o.a. Ausnahme-Typen bekannt sind. Wie? Nun ja, indem die Methode diese dem Client mitteilt. Das war wohl zu spontan. (q.e.d.)
Façade-Pattern
Das Façade-Pattern:
Schutz des Clients vor sinnloser Komplexität
Das Façade-Pattern schützt Clients vor einem komplexen Subsystem, indem es deren Service in eine einfach zu handhabende Schnittstelle kapselt (siehe Abb. 7.7).
Abbildung 7.7 Façade-Pattern
7.8.2 Design neuer Ausnahmen
Die Umsetzung des Façade-Patterns besteht im Design eigener einfacher Ausnahmen, welche die der verwendeten Packages ersetzen. Betrachten wir hierzu zuerst das Design der Java-Plattform. Es gibt zwei Extreme.
Design nach dem Standard-Package-Muster
Design von
Ausnahmen à la Standard-
Plattform
Die verschiedenen Ausnahme-Hierarchien der Java-Standard-Plattform sind leider kein leuchtendes Beispiel der Objekt-Orientierung.
Bis auf wenige Ausnahmen (etwa ParseException)19 sind alle Subklassen von Exception keine Spezialisierungen mit neuen Eigenschaften oder neuem Verhalten. Ausnahme-Instanzen unterscheiden sich – wenn überhaupt – nur in Eigenschaftswerten, d.h. in einer unterschiedlichen Meldung. Dies folgt dem Prinzip:
Pro Objekt eine Klasse.20 Dies bescherte der Java-Community eine stattliche Anzahl von Ausnahme-Klassen, die bereits im Namen ihre Objekt-Meldung tragen.
Design nach dem Muster SQLException
Design von
Ausnahmen à la java.sql
Das genannte Standard-Prinzip wurde zwar tapfer durchgehalten, nahm aber ein jähes Ende im java.sql-Package, welches nur noch die Ausnahme SQLException kennt.21
SQLException erklärt neue Fehlereigenschaften, die mit getSQLState() bzw. getErrorCode() abgefragt werden können, und sieht sogar eine Verkettung von SQLException-Instanzen vor.
Typsichere Konstanten: ein alternatives Design-Muster
Compiler-Aufgabe:
Überprüfung der Ausnahme-Syntax
Ein Grund, warum die Plattform-Entwickler jede Ausnahme-Art mit einem neuen Typ beantwortet haben, liegt wohl in der Typsicherheit. Wird eine Ausnahme hinter throw, throws oder catch fehlerhaft geschrieben, erkennt dies der Compiler sofort, ein smartes Feature!
Die Alternativen, Ausnahmen wie z.B. NullPointerException mittels Strings "NullPointerException" oder Konstante
static final int NULL_POINTER_EXCEPTION= 43;
zu identifizieren, leiden unter dem Problem, dass der Compiler beliebige, auch fehlerhafte Strings oder Zahlen akzeptiert. Das ist nicht akzeptabel!
Typsichere Alternative:
Design von
Ausnahmen: type-save Constants
|
Unterschiedliche Ausnahme-Arten in einem Package werden durch verschiedene Instanzen (nicht Subtypen!) repräsentiert. Die Ausnahme-Arten werden mit Hilfe von typsicheren (type-safe) Konstanten identifiziert. |
Ergebnis: Der Compiler ist in der Lage, fehlerhafte Angaben im Code zu erkennen, und trotzdem gibt es nur eine Ausnahme-Klasse.
Idiom zur typsicheren Konstante
Klassen-Muster
zu type-save Constants
Abbildung 7.8 Basis-Muster für typsichere Konstanten
Zum Muster gehört eine final erklärte Klasse, die die Konstanten enthält, und den einzigen Konstruktor private deklariert. Clients können somit nur die Konstanten nutzen (Abb. 7.8).
Das Idiom hat Variationen, z.B. ob man den Wert null als Konstante zulassen will oder ob man noch zusätzliche Eigenschaften benötigt.
Des Weiteren könnte man die Klasse TypesafeConstClass über ein zusätzliches Interface nutzen, um bei der Angabe der Konstanten den lästigen Klassennamen zu vermeiden.22
Deklaration von
typsicheren
Konstanten
Verwenden wir das Idiom in der Variante »mit einer Eigenschaft«:
// typsichere Konstanten zu Ausnahmen
final class TempErr { // immutable!
public final String msg; // blank final
public static final TempErr RangeErr= new TempErr("Range");
public static final TempErr ParseErr= new TempErr("Parse");
public static final TempErr TextOpenErr= new TempErr("TextOpen");
//...
private TempErr(String s) { msg= s; };
public String toString() { return msg; }
}
Deklaration von
typsicheren
Ausnahmen
// checked Exception: sehr minimalistisch, ohne
Meldung!
class TempException extends Exception { // immutable!
public final TempErr Err;
public TempException(TempErr tempErr) {
if (tempErr==null) throw new NullPointerException();
Err= tempErr;
}
}
Abbildung 7.9 Erweitertes Messwert-Import-Modell
In der Klasse TempValue ist nur die Umsetzung auf die Ausnahme TempException und die Einhaltung des Immutable-Idioms interessant.
Einsatz von
typsicheren Ausnahmen am Beispiel TempValue
class TempValue { // immutable!
private static DateFormat // Format: tt.mm.yy
df= DateFormat.getDateInstance(DateFormat.SHORT);
public static double minTemp;
public static double maxTemp=-1.0;
private java.util.Date d;
private double temp;
// muss aufgerufen werden, da maxTemp= -1.0
public static boolean setTempRange(double minT,double maxT) {
if (minT<=maxT) {
minTemp= minT; maxTemp= maxT; return true;
}
return false;
}
public TempValue(String sDate, String sTemp)
throws TempException {
try {
d= df.parse(sDate); // parse ist sehr tolerant
if (!sDate.equals(df.format(d))) // deshalb noch ein Check ¨
throw new TempException(TempErr.ParseErr);
temp= Double.parseDouble(sTemp);
}
catch (Exception e) { //ParseException, NumberFormatException
throw new TempException(TempErr.ParseErr);
}
if (minTemp > temp || temp > maxTemp)
throw new TempException(TempErr.RangeErr);
}
// getDate() verletzt Kompositition, d.h. immutable
// java.util.Date getDate() { return d; }
String getStringDate() { return df.format(d);
}
double getTemp() { return temp; }
}
Erklärung: Damit TempValue immutable bleibt, darf getDate() keine Referenz von d nach außen liefern (siehe hierzu auch 4.2.5).
Der String-Vergleich bei ¨ prüft zusätzlich die Gültigkeit des Datums im Format tt.mm.yy, denn die Methode parse() akzeptiert z.B. auch "60.12.99" und rechnet dies einfach in "29.01.00" um.
7.8.3 Ein Idiom zur Behandlungs-Strategie
Die Methode setToleranz() ist aus der Sicht der Behandlungen diverser Ausnahmen interessant. Man hat die Wahl zwischen einem schwer durchschaubaren Spagetti-Code oder einem einfachen Idiom:23
Ausnahme-Behandlung23
Behandlungs-Strategie zu
Ausnahmen
|
Vermeide – sofern die Semantik es zulässt – try-catch-Anweisungen für jede einzelne Methode.24 |
|
Reagiere zuerst auf Kontrakt-Verletzung. |
|
Behandle in einer einzigen äußeren try-catch-Anweisung alle fatalen Ausnahmen und in einem inneren try-catch die nicht fatalen. |
|
Verwende das Cleanup-Idiom (3.3.4) und verlagere alle notwendigen abschließenden Aktionen in den finally-Block. |
|
Behandle alle Ausnahmen, die durch die Aktionen im finally-Block entstehen, in finally selbst (siehe 7.3.3). |
|
Code-Muster
zum Idiom
Code-Muster zum Idiom24
// vorab die Kontrakt-Prüfung, dann
try {
Aktionen mit fatalen Fehlern!
try {
Aktionen mit nicht fatalen Fehlern!
}
catch (NonFatalException1 e) {...}
catch (NonFatalException2 e) {...}
...
}
catch (FatalException1 e) {...}
catch (FatalException2 e) {...}
...
finally {
try { abschließendes Cleanup }
catch(Exception e) {...}
}
7.8.4 Behandlungs-Strategie beim Messwert-Import
Umsetzung des Idioms zu Ausnahme-Behandlungs
in der Klasse TempImport
Die Implementation von TempImport folgt dem o.a. Behandlungs-Muster, wobei die innere try-catch-Anweisung in einer while-Anweisung eingebettet ist.
Erklärungen werden direkt vor die einzelnen Anweisungen gestellt.
class TempImport {
private static double parseERate= 0.01; // default 1%
private static double tempERate= 0.01; // default 1%
// -------------------------------------------------------
// erwartet Dateiname und eine Kollektion
// die Übergabe als Interface-Argument erlaubt
// 1. die Wahl der Collection-Klasse durch den Client
// 2. die Herausgabe von Messwerten, auch
// wenn eine Ausnahme aufgetreten ist.
// -------------------------------------------------------
public static void importTemp(String fileName,
Collection tempValues)
throws TempException {
// --- der Kontrakt heißt: keine null-Argumente
---
Kontrakt-Missbrauch mit RuntimeException beantworten
if (fileName== null || tempValues==null)
throw new NullPointerException(
"fileName oder tempValues null");
BufferedReader txt= null; // Textdatei
Connection con= null; // Datenbank-Verbindung
Statement stmt= null; // SQL-Statement
// ---- nun folgen die vielen Umweltprobleme
---
// ---- nur ein großes try, nicht viele einzelne ---
// ---- siehe Behandlungs-Idiom oben ---
Eine äußere
try-Anweisung für alle fatalen Fehler
try {
// Text-Datei mit Name fileName wird geöffnet
// fataler Fehler löst FileNotFoundException aus!
txt= new BufferedReader(new FileReader(fileName));
// Klasse mit dem JDBC-ODBC-Treiber wird geladen
// fataler Fehler löst ClassNotFoundException aus!
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
String s;
int numLines= 0, // Anz. nicht leerer Messwert-Zeilen
numPErr=0, // Anz. Parse-Fehler
numRErr=0; // Anz. Range/Intervall-Fehler
// ---- zeilenweise aus Textdatei einlesen
----
// ---- fataler Lesefehler löst IOException aus ----
while ((s= txt.readLine())!= null) {
// ---- leere Textzeilen
sind zu überspringen ----
if (!(s= s.trim()).equals("")) {
numLines++;
// ---- String in Token (Datum,Temp) zerlegen
----
// ---- Trenner sind white spaces ----
StringTokenizer st= new StringTokenizer(s);
// ---- es folgen die nicht fatalen Fehler
----
// ---- in einem inneren try ----
Eine innere
try-Anweisung für alle nicht fatalen Fehler
try {
// ---- zufügen zur Collection
hinzufügen
// ---- löst nicht fatale Fehler aus
tempValues.add(
new TempValue(st.nextToken(),st.nextToken()));
}
// ---- zählen und weitermachen
----
Behandlung
aller nicht
fatalen Fehler
catch (NoSuchElementException e) { numPErr++;
}
catch (TempException e) {
// ---- Unterscheidung der Ausnahme
----
if (e.Err == TempErr.ParseErr) numPErr++;
else numRErr++;
}
}
}
// ---- wenn keine Messwerte vorhanden ‡ Ende ----
// ---- da sonst ArithmeticException ----
// ---- wichtig: finally kommt vor return ----
if (numLines==0) return;
// ---- sind die nicht fatalen Fehler fatal?
----
if (numPErr/numLines>parseERate)
throw new TempException(TempErr.ParseErr);
if (numRErr/numLines>tempERate)
throw new TempException(TempErr.RangeErr);
// ---- Verbindung zur Datenbank aufbauen
----
// ---- fatale Fehler lösen SQLException aus ----
con= DriverManager.getConnection("jdbc:odbc:AccessTest1");
stmt= con.createStatement();
// ---- Iteration durch die Messwert-Kollektion
----
for (Iterator i= tempValues.iterator(); i.hasNext();
) {
TempValue tv= (TempValue) i.next();
// ---- Einfügen in die Datenbank
----
stmt.executeUpdate("INSERT INTO MESSWERTE
VALUES ('"+
tv.getStringDate()+"',"+
Double.toString(tv.getTemp())+")");
}
}
// ---- zentrale Behandlung aller fatalen Fehler
----
// ---- und Umwandlung in eine TempException ----
Behandlung aller
fatalen Fehler
catch (FileNotFoundException e) {
throw new TempException(TempErr.TextOpenErr);
}
catch (ClassNotFoundException e) {
throw new TempException(TempErr.JDBCDriverErr);
}
catch (IOException e) {
throw new TempException(TempErr.TextReadErr);
}
catch (SQLException e) {
// ---- Abfrage auf 08001 ist nicht so toll
----
if (e.getSQLState().equals("08001"))
throw new TempException(TempErr.ConnectErr);
throw new TempException(TempErr.SQLExecErr);
}
Cleanup im
finally-Block
// ---- nicht null ‡ Öffnen war erfolgreich
----
// ---- Cleanup-Aktion: Schließen ----
finally {
// ---- ohne try-catch würde hier eine Ausnahme ----
// ---- alle fatalen Fehler ersetzen ----
try {
// siehe zu dieser Lösung abschließenden
Kommentar
Keine Resume-Next-Logik
if (txt!= null) txt.close();
if (con!= null) con.close();
if (stmt!= null) stmt.close();
} catch (Exception e) {}
}
}
// -------------- Ende von importTemp() ----------------
public static boolean setTempRange(double minT,
double maxT) {
return TempValue.setTempRange(minT,maxT);
}
public static boolean setToleranz(double parseErrRate,
double tempErrRate) {
if (0 <= parseErrRate && parseErrRate < 1 &&
0 <= tempErrRate && tempErrRate < 1) {
parseERate= parseErrRate; tempERate= tempErrRate;
return true;
}
return false;
}
}
Typensichere
Ausnahmen:
Die Verwendung der Import-Klasse kann wieder als Code-Fragment dargestellt werden:
Nur ein catch-Block, keine Typvergleiche mit instanceof
TempImport.setTempRange(-10.0,30.0);
Collection c= new java.util.ArrayList();
try {
TempImport.importTemp("C:/Messwerte/TempValues.txt",c);
}
catch (TempException e) {
System.out.println(e);
System.out.println(e.Err); // Art der Ausnahme!
}
Im abschließenden finally-Block von importTemp() werden alle geöffneten Ressourcen wieder geschlossen. Wirklich alle?
try-Anweisung und das
Ignorieren von Ausnahmen
Nein, denn der Code basiert auf der einfachen Annahme, dass alles, was geöffnet wurde, auch geschlossen werden kann. Beteiligt daran ist das try-catch, welches beim ersten Fehler, z.B. verursacht durch eine txt.close-Anweisung, abbricht, und damit – man ahnt es bereits – die restlichen Ressourcen con, stmt offen lässt.
In diesem Fall ist die Lösung einfach, unelegant und simuliert eine »On Error Resume Next«-Logik25 :
if (txt!=null) try { txt.close(); } catch (Exception
e) {}
...
Damit ist auch die Einschränkung »sofern die Semantik es zulässt« im ersten Punkt des Idioms zur Ausnahme-Behandlung erklärt.
7.9 Zusammenfassung
Ohne ein schlüssiges Ausnahme-Konzept ist das Schreiben von robusten Klassen extrem schwierig, wenn nicht unmöglich.
Java bietet ein Ausnahme-Konzept, welches Fehler auf der Basis »not recoverable« behandelt. Unterstützt wird es vom Compiler bzw. der JVM und einer umfangreichen Ausnahme-Hierarchie in der Plattform.
Anhand der Unterscheidung der Subtypen Error, checked und unchecked Exception können Laufzeitfehler differenziert behandelt werden.
Der Ausnahme-Mechanismus bietet mit dem finally-Block immer die Möglichkeit von Cleanup-Aktionen an, selbst wenn fatale Fehler aufgetreten sind.
Bei der Deklaration eigener Ausnahmen im Server und der Behandlung von Ausnahmen beim Client helfen u.a. ein Façade-Pattern sowie einige Idiome.
Am Beispiel »Messwert-Import« werden alle vorgestellten Design- bzw. Behandlungs-Hinweise und Idiome abschließend demonstriert bzw. diskutiert.26
7.10 Testfragen
Zu jeder Frage können jeweils eine oder mehrere Antworten bzw. Aussagen richtig sein.
throw new EClass();
class MyException extends Exception {}
abstract class B {
abstract void f() throws Exception;
abstract void g();
}
class D extends B {
f() /* ... */ { /*...*/ }
g() /* ... */ { /*...*/ }
}
class ExTest {
static void f(int i, String s) {
if (s=="") throw new NullPointerException();
System.out.println (s);
System.out.println (1/i);
}
}
Welche Meldungen sind Teil der Ausgabe der folgenden Anweisung?
ExTest.f(0,null);
8. |
Es ist folgende Klasse deklariert: |
class CatchTest {
static void f(String s) throws Exception {
try { int i= Integer.parseInt(s); }
catch (Exception e) {
System.out.println("F1"); throw e;
}
finally { System.out.println(s); return; }
}
}
Welche Zeilen sind Teil der Ausgabe der folgenden Anweisung?
try { CatchTest.f("a"); }
catch (Exception e) { System.out.println("F2"); }
class ThrowTest {
void f() {
if (true) throw new RuntimeException(); // 3
if (true) throw new RuntimeException(); // 4
if (true) throw new Error(); // 5
if (true)
try { throw new Exception(); } catch (Error e){} // 7
if (true)
try { new Exception(); } catch (Throwable e){} // 9
}
}
1 In Englisch sehr anschaulich mit »Throwing an exception« beschrieben.
2 Werden Ausnahmen auch zur Kommunikation in »außergewöhnlichen Fällen« benutzt, ist das Konzept zu inflexibel. Leider gibt es diese Situation auch in der Java-Sprache selbst, u.a. bei der Kommunikation von Threads (siehe Kapitel 9, Threads).
Dies führt dann zu einem unpassenden Programm-Design, da um die Defizite in Java »herumprogrammiert« werden muss.
3 Leaf-Klassen haben keine Subklassen mehr.
4 Sollte der Grund nicht klar sein, unbedingt das Marker-Interface-Idiom in 6.10 nacharbeiten!
5 Der Stack Trace sollte natürlich nur für das Debugging von Code interessant sein und weniger für den genervten Kunden.
6 Jedoch kann z.B. auch eine Ausnahme vom Typ VerifyError mit Hilfe einer inneren Klasse provoziert werden, welches kein Sicherheitsproblem, sondern eine Inkonsistenz der JVM aufdeckt (siehe Kapitel 8, Innere Klassen).
7 Siehe hierzu 7.7.2
8 Man denke nur an Programme zur Maschinensteuerung, die bei Umweltfehlern nicht einfach mit Abbruch oder Konsol-Meldungen à la Dr. Watson reagieren können.
9 Unchecked Exceptions sind ausgenommen und können beliebig deklariert oder weggelassen werden.
10 Zur Unchecked-Variante siehe 7.7.2.
11 Das ist zu trivial, da dann auch Teile-Nummern wie »--1--234--« zugelassen wären.
12 Das Thema ist deshalb so heikel, weil beim Ausnahme-Design zur Sun-Plattform der bekannte MS-Grundsatz »Unsere Fehler sind Features« befolgt wurde (siehe 7.8.2).
13 Sofern man nicht eine this-Referenz vor der Ausnahme nach außen gereicht hat.
14 Siehe Beispiel zur Checked-Variante in 7.6.
15 Im Sinn der UML ist mit einem Signal ein Signal-Objekt verbunden.
16 Eine Audio/Videoausgabe wird nicht gleich wegen eines fehlerhaften Datensignals abgebrochen.
17 Endlose Fallunterscheidungen als Reminiszenz an das dunkle Prä-OO-Zeitalter.
18 Die Instanz wird nicht angelegt.
19 Zum Beispiel ParseException
20 Dies steht im krassen Gegensatz zur Klassifizierungs-Regel in 4.1.1.
21 Das alte Muster wäre aufgrund der unzähligen datenbankspezifischen Ausnahmen gegen einen gewaltigen Exception-Baum gefahren. Interessant, SQLWarning ist zwar eine Subklasse von SQLException, wird aber nicht mit throw ausgelöst.
22 Dies bedeutet allerdings, dass der Konstruktor nicht private sein darf.
23 Die Relevanz des Idioms wird zur Übung erklärt (Hilfe: Man programmiere einfach die Methode importtemp()).
24 Dies ist nicht immer möglich, z.B. dann nicht, wenn Fehler ignoriert werden müssen (siehe hierzu Endkommentar zur Implementation von importTemp()).
25 Visual Basic-Jargon, siehe auch 7.1.
26 Ein schönes delegations-basiertes Aufnahmesystem fiel leider dem Rotstift zum
Opfer, da es inklusive eines passenden Beispiels den Rahmen des Kapitels gesprengt hätte.
|