
|
J2SE-API Java 2 Standard Edition - Application Programming Interface. Beschreibung der Programmierschnittstellen der Pakete, die mit der Java Plattform ausgeliefert werden. |
|
JUnit Ein Open Source Framework zur Durchführung von automatisierten Tests von Java-Programmen. |
|
JVM Java Virtual Machine. Programm, in dem Java-Programme ausgeführt werden. Eine JVM ist vom Betriebssystem abhängig, während das Java-Programm selbst unabhängig vom Betriebssystem ist. |
|
Log4j Ein Open Source Framework der Apache Software Foundation zum Protokollieren von Anwendungsmeldungen in Java. |
|
Stacktrace Ein Stacktrace steht zur Verfügung, wenn es zu einem Ausnahmezustand kommt. Mit seiner Hilfe kann die Aufrufkaskade rekonstruiert werden. |
Weiterführende Links
|
| Abb. 1: Exception-Klassenhierarchie. checked exceptions sind vom (Sub-) Typ Exception, unchecked exceptions sind vom (Sub-)Typ Error und RuntimeException. |
|
Tipp 1: RuntimeExceptions sind zwingend zu vermeiden. Robuster Code muss so geschrieben werden,
dass Ausnahmen vom Typ RuntimeException nicht mehr auftreten. |
| Abb. 2: Tipps zum Exception Handling. |
@org.junit.Test(expected=IndexOutOfBoundsException.class)
public void testListIdx() {
List list = new ArrayList();
list.get(0);
}
|
| Abb.3: Mit JUnit lässt sich problemlos das erwartete Auftreten von Ausnahmen testen. Der Test schlägt fehl, wenn die Liste nicht leer ist. |
catch (ClassNotFoundException e) {
LOG.error("Blah", e);
throw e;
}
catch (ClassNotFoundException e) {
LOG.error("Blah", e);
throw new MyServiceException("Blah", e);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
throw new MyServiceException("Blah");
}
|
| Abb. 4: Negativbeispiel. Abfangen der Ausnahme, Logging bzw. Stacktrace-Ausgabe und anschließendes Weiterleiten mit throw. |
public void foo() throws Exception {...}
public void foo() throws MyException,
AnotherException, SomeOtherException,
YetAnotherException {...}
try {
foo();
} catch (Exception e) {
LOG.error("foo failed", e);
}
|
| Abb. 5: Die Deklaration und Behandlung von Ausnahmen sollte bezüglich des Exception-Typs möglichst detailliert sein. Exception allein ist unzureichend. |
catch (NotSuchMethodException e) {
;
}
catch (NotSuchMethodException e) {
return null;
}
try {
blah();
} finally {
cleanUp();
return null ;
}
|
| Abb. 6: Leere catch-Blöcke oder lediglich ein return sowie return im finally-Block führen zum Verlust der Ausnahme. |
Die Aufgabe der Entwicklung ist es, Daten und Prozesse entsprechend der Spezifikation zu verarbeiten. Hierzu werden Werkzeuge gesucht und Software-Lösungen realisiert. Doch mit der Umsetzung ist die Arbeit noch nicht beendet!
Was ist, wenn die Ausgangsdaten in der Datenbank nicht gefunden werden, die Datei nicht geparst werden kann, bei der Berechnung durch Null dividiert wird, der Nutzer über die GUI eine negative Zahl angegeben hat oder in verteilten Systemen benötigte Sub-/Fremdsysteme nicht zur Verfügung stehen? Ausnahmen dieser Art sind unvermeidbar, sie sind die Regel - keine Regel ohne Ausnahmen.
Wesentlicher Vorteil der Ausnahmebehandlung durch Exception Handling ist zunächst einmal die syntaktische Trennung der Ausnahmen von der regulären Datenverarbeitung. Bei der Verwendung von Exceptions wird der Programmfluss nicht durch Abfrage des Rückgabestatus unterbrochen. Ein spezieller Programmbereich überwacht potentielle Ausnahmen und ruft gegebenenfalls einen Programmcode zur Behandlung auf.
Bei Java-Entwicklungen tauchen häufig folgende typische Unsicherheiten auf:
Wer kümmert sich eigentlich um die Ausnahmen? Was ist der Unterschied zwischen checked und unchecked exceptions und was hat es mit dieser Unterscheidung auf sich? Gibt es nicht doch bessere Ansätze als einfach try/catch/printStackTrace()? Warum stürzt das Programm ohne Fehlermeldung ab? Existiert da tatsächlich irgendwo ein leerer catch-Block? Wie ich den Fehler beheben kann, weiß ich nicht. Aber ein System.err.println(...)ist schnell eingetippt. Besser wäre aber ein standardisiertes Protokollieren (Logging). Schön wäre auch zu wissen (testen), ob eine Ausnahme wie erwartet ausgelöst wird.
Ausnahmen sind nichts anderes als Objekte aus der Exception-Klassenhierachie (siehe Abbildung 1). Alle Ausnahmen sind mindestens von Throwable abgeleitet, d. h. sie können prinzipiell erst einmal ausgelöst werden. Bei den Exception-Klassen sind generell zwei verschiedene Typen zu unterscheiden: geprüfte Ausnahmen (checked exceptions) und nicht geprüfte Ausnahmen (unchecked exceptions).
Die beiden Begriffe checked bzw. unchecked kategorisieren Ausnahmen, die entweder im Code geprüft oder deklariert (checked) bzw. nicht geprüft werden müssen (unchecked).
Bei den checked exceptions überwacht der Compiler die festgelegten Regeln: wo und wann können sie auftreten bzw. müssen sie behandelt werden. Die unchecked exceptions können zu jedem Zeitpunkt von der JVM oder auch explizit durch einen Code ausgelöst werden. Sie führen ohne Behandlung immer zum Abbruch der Ausführung bzw. des laufenden Threads. Die Unterscheidung zwischen checked und unchecked ergibt sich ganz einfach aus der Klassenhierarchie (siehe Abbildung 1): Zu den unchecked exceptions gehören alle Error- und RuntimeException-Klassen, zu den checked exceptions alle anderen Klassen
Ausnahmen vom Typ RuntimeException sind unchecked und signalisieren - einfach gesagt - technische Fehler im Code (Programmier- und/oder Denkfehler), die der Compiler nicht aufdecken kann. So führt z. B. eine ganzzahlige Division durch Null zu einer ArithmeticException und ein ungültiger Indexwert beim Zugriff auf Array-Elemente zu einer ArrayIndexOutOfBoundsException(siehe Tipp 1 in Abbildung 2).
Der Index des Arrays und die Objektreferenz zur Vermeidung der NullPointerException sind genauso zu prüfen, wie die Gültigkeit arithmetischer Operationen.
Zu den unchecked exceptions gehören auch die Error-(Sub)-Klassen. Diese gelten in der Regel als nicht behebbar und sollten folglich im Code nicht abgefangen werden. Es ist kaum möglich, aus dem Programm heraus zum Beispiel LinkageError (die Klasse kann zur Laufzeit nicht eingebunden werden) oder OutOfMemoryError sinnvoll zu behandeln. Hier muss die Ursache analysiert und "von außen" behoben werden (siehe Tipp 2 in Abbildung 2).
Checked exceptions sind dagegen Ausnahmen, die durch den Programmfluss bzw. der Programmumgebung und nicht durch einen fehlerhaften Code verursacht werden.
Hierunter fallen z. B. Klassen, die nicht gefunden werden (ClassNotFoundException), Ein- und Ausgabefehler (IOException) oder auch fachliche- und applikationsspezifische Ausnahmen, für die i. d. R. eigene Exception-Klassen existieren. Diese Fehler müssen in einem robusten Programm unbedingt abgefangen werden. Außerdem müssen - je nach Aufgabe - Strategien implementiert werden, wie diesen Fehlern (situationsabhängig) zu begegnen ist. Auf Basis der vorliegenden Fehlerinformationen sollte es möglich sein, sinnvoll reagieren zu können, z. B. in Form von Alternativen, Wiederherstellungs-Mechanismen, Verbindungsauf-/abbau etc. Ziel dabei ist es, das Programm in einem stabilen Zustand zu halten, gegebenenfalls unter akzeptablen Einschränkungen (siehe Tipp 3 in Abbildung 2).
Eine ParseException, die beispielsweise bei der Umwandlung einer Zeichenkette in ein Datum auftritt, zeigt an, dass die Zeichenkette nicht das korrekte Format hat. Bei einer Benutzerinteraktion könnte die Behandlung so aussehen: Anzeige eines entsprechenden Hinweises mit der Aufforderung zur Neueingabe im passenden Format
Ohne Benutzerinteraktion ist die Situation schon schwieriger. Wird ein ungültiges Datumsformat an die Methode übergeben, so kann die Methode diese Ausnahme nicht beheben. Sie hat jetzt nur die Möglichkeit, die ParseException an den Aufrufer weiterzuleiten (siehe Tipp 4 in Abbildung 2).
Um sicher zu stellen, dass angeforderte Ressourcen wieder freigegeben werden, eignet sich der Finally-Block.
Das korrekte Auslösen von Exceptions lässt sich sehr leicht mit JUnit-Tests überprüfen. Entweder mit der Methode fail() oder ab JUnit 4.x noch einfacher mit dem Attribut expected=Throwable1[,...ThrowableN] (siehe Abbildung 3).
Insbesondere kritische Programmbereiche, z. B. der Persistenz-Layer von JEE-Anwendungen, erfordern Robustheit und Zuverlässigkeit. Hier sollte unbedingt das Verhalten bei Störungen während Transaktionen und das Funktionieren des Rollbacks getestet werden (siehe Tipp 5 in Abbildung 2).
Interessant und hilfreich ist auch der Einsatz von Assertions (ab Java 1.4), um die Stabilität weiter zu erhöhen und mögliche Fehler gänzlich zu vermeiden. Über dem Befehl assert Expression1 [: Expression2] sind Zusicherungen auf der Compiler-Ebene überprüfbar, z. B. ob einer Methode ungeeignete bzw. verbotene Parameterwerte übergeben wurden. Liefert der Ausdruck false, erzeugt die JVM ein AssertionError.
Problemlösungen beim Auftreten einer Ausnahme sind selten. In vielen Fällen werden die Ausnahmen daher nur in Form einer allerletzten Eskalation behandelt: Die Ausnahme wird protokolliert (siehe Tipp 6 in Abbildung 2).
Logging, z. B. in eine Log-Datei oder eine SQL-Datenbank, ist eine große Hilfe während der Entwicklungszeit oder auch später im produktiven Einsatz. Log4j ermöglicht über eine Steuerdatei das selektive Ein- und Ausschalten von Log-Meldungen. Ausgaben über System.out... oder System.err... im catch-Block sind unbrauchbar. Die Konsole ist nicht immer sichtbar, die Meldungen sind statisch und zur Laufzeit nicht konfigurierbar. Für die Ausgabe sollte immer und nur ein standardisierter Logging-Mechanismus benutzt werden (siehe Tipp 7 in Abbildung 2)!
Wichtig hierbei ist ein festgelegtes und einheitliches Format der Log-Einträge, damit die Ausgaben später mit Tools auswertbar sind. Der Einsatz von Exception-ID's vereinfacht in verteilten, mehrschichtigen Multithreading-Systemen die Zuordnung und Identifizierung von Fehlern (siehe Tipp 8 in Abbildung 2).
Die Varianten der Fehlerbehandlung in Abbildung 4 sind Negativbeispiele. Durch solche Catch-Blöcke könnte es sehr leicht passieren, dass die Exception weiter oben in der Aufrufhierarchie erneut protokolliert wird.
Mit der Kombination aus Fehlerausgabe und Weiterleiten (throw) entstehen sehr oft doppelte Log-Einträge bzw. Stacktrace-Ausgaben. Die Fehleranalyse und Zuordnung des Stacktraces wird dadurch unnötig erschwert. Redundante Log-Einträge kann niemand gebrauchen (siehe Tipp 9 in Abbildung 2).
Auch die Umwandlung der abgefangenen ClassNotFoundException in eine MyServiceException ist kritisch. Die Verwendung eines neuen Exception-Typs ist nur dann sinnvoll, wenn der Aufrufer dadurch weitere Informationen oder Reaktionsmöglichkeiten erhält. Das heißt, die Klasse MyServiceException müsste mindestens zusätzliche Schnittstellen haben. Ansonsten verschwendet allein die Objekterzeugung nur Ressourcen und es entsteht kein Mehrwert, es sei denn, die Umwandlung ist technisch bedingt, wie z. B. in JEE-Anwendungen, wo bestimmte Exception-Typen nicht beim Client ankommen dürfen oder sollen (siehe Tipp 10 in Abbildung 2).
Die Basisklasse java.lang.Exception ist mit Vorsicht zu benutzen. Exception ist einfach zu unspezifisch und viel zu generisch. Insbesondere der (bewusste) Einsatz von checked exceptions wird hierdurch vereitelt, denn auch RuntimeException ist als Unterklasse typkompatibel und somit auch eine Exception. Durch ein einfaches throws Exception (siehe Abbildung 5) bekommt der Aufrufer lediglich die Information, dass irgendetwas in der Methode schief gehen kann. Existieren mehrere potentielle Fehlerquellen/-ursachen, auf die ein Client unterschiedlich reagieren kann, sollten die zugehörigen Exception-Typen auch entsprechend deklariert sein. Ansonsten geht dem Aufrufer ein wichtiger Detaillierungsgrad bezüglich der Ursache und der Reaktion verloren.
Ähnlich problematisch wie mit throws Exception verhält es sich auch mit catch(Exception). Sollte die aufgerufene Methode foo() später zusätzliche Exceptions deklarieren, ist der Code compiler-technisch weiterhin korrekt. Alle weiteren Exception-Typen werden ungemerkt mit abgefangen. Die neue Exception geht quasi verloren und ist damit praktisch wertlos (siehe Tipp 11 in Abbildung 2).
Ausnahmen können leider sehr mühelos verloren gehen. Der Sourcecode in Abbildung 6 ist compilierbar und zeigt den worst case der Ausnahmebehandlung. Die aufgetretene NotSuchMethodException geht für immer und ewig verloren, inklusive dem Stacktrace. Die Idee von Aufräumaktionen (z. B. Ressourcenfreigabe) in cleanUp() innerhalb des Finally-Blocks ist ein guter Ansatz. Aber auch hier ist Vorsicht geboten: return im Finally-Block unterdrückt die Ausnahme, sie wird nicht nach außen geliefert. Und sollte cleanUp() selbst eine Exception werfen, geht die NotSuchMethodException ebenfalls verloren.
Die Folgen sind gravierend. Es kommt zu nicht reproduzierbaren Programmabstürzen, ohne die geringste Chance der Fehleranalyse. Wenn eine Behandlung nicht möglich ist, muss mindestens ein e.printStackTrace() herhalten -besser natürlich ein Log-Eintrag (siehe Tipp 12 in Abbildung 2)!
Exception Handling ist eine interessante und anspruchsvolle Aufgabe. Wichtig ist das Bewusstsein, dass Fehler und Ausnahmen auftreten werden. Der korrekte und pflichtbewusste Umgang mit diesen Situationen zählt ebenso zu den Aufgaben des Entwicklers wie etwa eine korrekte Programmlogik und Datenverarbeitung.
|
Teil I: Java-Objekte in der Identitätskrise Teil II: Konfiguration der IDE Teil III: Typisch Ant Teil IV: Exception Handling Teil V: Software-Engineering |