Unter einer Ausnahme (Exception) versteht man das Auftreten eines Ereignisses während des Programmablaufs, das den normalen Abarbeitungsfluss unterbricht. Derartige Fehler können bspw. aus Hardware-Problemen oder Programmierfehlern resultieren. Tritt eine Exception in einer Java-Methode auf, so erzeugt diese ein Exception-Objekt und übergibt dieses dem Laufzeitsystem. Das Exception-Objekt beinhaltet Informationen über die Art der Exception, deren Typ und den Status des Programms zu dem Zeitpunkt, zu dem der Fehler auftrat. Anschließend ist das Laufzeitsystem dafür verantwortlich, den Fehler geeignet zu bearbeiten. Hierzu ist eine Routine zu lokalisieren, die die Fehlerbearbeitung übernimmt. Das Laufzeitsystem durchsucht rückwärts die aufgerufenen Methoden, bis sie in einer der Klassen eine Routine findet, die den Fehler beheben kann (sog. Exception Handler). Dieser Vorgang ist in Abb. 3-12 dargestellt. Ein Exception Handler kann einen Fehler dann bearbeiten, wenn der Typ der Exception dem entspricht, den der Handler verarbeiten kann. Die Exception wird daher solange in entgegengesetzter Richtung der Aufrufreihenfolge weitergereicht, bis eine Klasse gefunden wird, die einen entsprechenden Exception Handler enthält. Kann das Laufzeitsystem keinen geeigneten Exception Handler finden, so wird das Programm abgebrochen. Hieraus wird auch die Bedeutung erkennbar, die das Schreiben von geeigneten Exception Handlern hat. Abb. 3.12: Lokalisierung eines Exception Handlers Die Verwendung von Exceptions zur Verarbeitung von Fehlern hat einige entscheidende Vorteile: Code zur Bearbeitung von Fehlern ist vom regulären Code getrennt, wodurch die Lesbarkeit von Programmen verbessert wird, Fehler können entgegengesetzt der Aufrufreihenfolge weitergereicht werden. Im Regelfall kann daher ein einziger Exception Handler Fehler verarbeiten, die von einer ganzen Reihe von Klassen verursacht werden. Fehlertypen können in Klassen eingeteilt und dementsprechend spezifisch durch eine Menge verschiedener Exception Handler verarbeitet werden. Durch diese Trennung kann bessere Lesbarkeit der einzelnen Handler erreicht werden, als wenn die gesamte Fehlerbearbeitungsroutine in einer einzigen Methode angeordnet wäre. Zudem können speziellere Exception Handler implementiert werden, indem allgemeinere Exception Handler ihre Funktionalität auf Subklassen vererben. Spezifikation von Exceptions In Java kann eine Methode eine Exception entweder an einen Exception Handler weiterreichen oder innerhalb der Methode selbst verarbeiten, wozu die Bedingungen, die zur anschließenden Verarbeitung des Fehlers führen, spezifiziert werden müssen. Eine Methode kann eine Exception abfangen, indem sie selbst einen Exception Handler implementiert. Ist dies hingegen nicht gewünscht, so muss die Methode angeben, dass eine derartige Exception vorkommen kann und dass diese damit an anderer Stelle verarbeitet wird. Durch ein derartiges Vorgehen wird bereits garantiert, dass eine Anwendung nicht unvorhergesehen abstürzt (falls alle möglichen Exceptions angegeben werden). Ein weiterer Vorteil liegt darin, dass die Methode bereits vor dem Aufruf mitteilt, welche Fehler möglicherweise auftreten können. Die Angabe der möglichen Exceptions erfolgt daher sinnvollerweise in der Signatur der Methode. In Java können eine Vielzahl von Exceptions auftreten, bspw. I/O- oder Laufzeitausnahmen. Laufzeit-Exceptions treten im Laufzeitsystem auf, bspw. in arithmetischen Berechnungen oder in Indizieroperationen. Da derartige Exceptions fast überall in einem Programm vorkommen können und die Erfassung sehr aufwendig wäre, verlangt der Compiler nicht, dass diese Art von Ausnahmen überall angegeben wird. Es sei aber darauf hingewiesen, dass ein Auslassen derartiger Definitionen zu Programmabstürzen führen kann. Eine weitere Art der Exceptions sind solche, bei denen der Compiler verlangt, dass sie entweder von der Methode abgefangen oder in der Signatur angegeben werden. Hierbei ist zu beachten, dass nicht nur solche Exceptions angegeben werden, die eine Methode unmittelbar auslöst, sondern auch indirekte, die sich durch Aufrufe anderer Methoden ergeben können. Abb. 3.13: Klasse Throwable Exceptions in Methodensignaturen Zur Definition des Exception-Verhaltens in einer Methodensignatur sind Objekte zu verwenden, die von der Klasse Throwable einfach oder mehrfach abgeleitet sind. Der Aufbau der Klasse Throwable ist in Abb. 3-13 dargestellt. Die Klasse Throwable vererbt direkt an die Subklassen Error und Exception. Mittels der Klasse Error werden solche Fehler verarbeitet, die auftreten, wenn Probleme beim dynamischen Linken oder sonstige schwere Fehler der Java-VM auftreten. Typischerweise sind solche Fehler derart schwer, dass sie nicht behoben und daher auch nicht abgefangen werden können. Die meisten Programme lösen Exceptions aus, die ihre Eigenschaften von der Klasse Exception erben. Exceptions sind daher nicht so schwerwiegend wie Errors. Die Klasse Exception verfügt über eine Vielzahl von Subklassen, die die verschiedenen Exceptions, die auftreten können, verarbeiten. Eine Subklasse, der eine besondere Bedeutung zukommt, ist die Klasse RuntimeException, die Exceptions repräsentiert, die in der Java-VM auftreten können. Ein Beispiel hierfür ist eine NullPointerException, die auftritt, wenn eine Methode auf ein Objekt zugreifen will, das nicht referenzierbar ist. Wie bereits erwähnt, erlaubt der Compiler das Auslassen der Definition von Laufzeit-Exceptions, da diese an einer Vielzahl von Stellen eines Programms auftreten können. Eine saubere Programmierung minimiert jedoch stets das Risiko, dass ein derartiger Fehler tatsächlich auftritt. Die Java-Packages definieren verschiedene RuntimeException-Klassen, die, wie jede andere Exception auch, abgefangen werden können. Eine spezielle Angabe in der Signatur einer Methode ist hierfür jedoch nicht nötig. Zusätzlich können benutzerdefinierte RuntimeException-Subklassen implementiert werden. Nachdem der Zusammenhang der definierbaren Exceptions verdeutlicht wurde, kann die eigentliche Erweiterung der Methodensignatur beschrieben werden. Alle Java-Methoden, die eine Exception auslösen können, müssen entweder in der Signatur der Methode das Schlüsselwort throws verwenden, dem die Art der auszulösenden Exception folgt, oder auftretende Exceptions innerhalb der Methode abfangen. Soll in einer Methode eine Exception explizit ausgelöst werden, so wird die Methode throw aufgerufen, die als Argument ein Objekt vom Typ Throwable erwartet. Das folgende Beispiel verdeutlicht die Verwendung dieser Kommandos. Hierbei wird eine BeispielException ausgelöst, die geeignet zu definieren ist (siehe unten). Eine weitere wichtige Eigenschaft der Klassendefinition ist in diesem Zusammenhang, dass Subklassen die throws-Signatur erben. In einer Subklasse wird daher immer mindestens dieselbe Menge von Exceptions ausgelöst, wie die in den Elternklassen definierten, ohne dass dies explizit in der Signatur angegeben werden müsste. Abfangen von Exceptions Jeder Exception Handler besteht aus den Anweisungsblöcken try und catch bzw. optional finally, deren Verwendung im Folgenden erläutert wird. Try-Anweisungsblock Der erste Schritt in der Implementierung eines Exception Handlers ist ein Try-Block, der die Anweisungen enthält, die eine Exception auslösen können. Verschiedene Arten von Exceptions werden dabei sukzessive in verschiedenen Try-Blöcken abgeprüft. Die Syntax eines Try-Blocks sieht folgendermaßen aus: try { Anweisungsfolge } Die in einem Try-Block eingeschlossenen Anweisungen können Exceptions auslösen. Ein Try-Block muss von mindestens einem Catch- oder einem Finally-Block gefolgt werden. Catch-Anweisungsblock Auf einen Try-Block folgt meist eine Sequenz von Catch-Blöcken, die die Ausnahmen angeben, die abgefangen werden sollen. Die allgemeine Syntax lautet daher wie folgt: try { Anweisungsfolge } catch (ThrowableObject Variablenname1) { Anweisungsfolge } catch (ThrowableObject Variablenname2) { Anweisungsfolge } Catch-Anweisungen erwarten als Argument eine Exception, deren Typ von der Klasse Throwable abgeleitet sein muss, die im Package java.lang definiert ist. Das Auslösen einer Exception entspricht dann der Erzeugung eines Exception-Objekts. Der Variablenname dieses Objekts gibt dem Handler die Möglichkeit, die Exception zu referenzieren. Innerhalb eines Catch-Blocks befinden sich Java-Anweisungen, die ausgeführt werden, wenn der Exception Handler aufgerufen wird. Das Laufzeitsystem ruft den Handler dann auf, wenn er der erste in der Abarbeitungsreihenfolge der Methoden ist, der dem Typ der ausgelösten Exception entspricht. In Java kann ein Exception Handler entweder nur eine (spezialisierter Handler) oder auch mehrere Exceptions verarbeiten. Da Exceptions Objekte vom Typ Throwable sind, sind sie nach der Auslösung Instanzen der Klasse Throwable oder einer der Subklassen von Throwable. Die Java-Packages enthalten eine große Anzahl an Klassen, die von Throwable abgeleitet sind und die in einer Klassenhierarchie angeordnet sind. Wird nun ein spezieller Exception Handler benötigt, so kann eine der Klassen verwendet werden, die sich auf unterster Ebene der Exception-Hierarchie befinden, die also keine Subklassen aufweisen. Diese Handler verarbeiten jeweils nur eine Exception. Wird im Gegensatz dazu eine der Klassen verwendet, die an Subklassen vererben, so können allgemeinere Handler geschrieben werden, die die Exception der Klasse selbst, aber auch die aller Subklassen bearbeiten. Im Allgemeinen sollte ein Exception Handler so speziell wie möglich sein. Handler, die eine ganze Reihe von Exceptions verarbeiten können, sind meist nutzlos, da sowieso festgestellt werden muss, welche Art der Exception aufgetreten ist, um die beste Fehlerbehebungsstrategie anwenden zu können. Allgemeine Handler sind zudem aufgrund ihrer Komplexität fehleranfällig, da eventuell Exceptions übergeben werden, für die der Handler nicht gedacht war. Finally-Anweisungsblock Finally-Blöcke werden dazu verwendet, einen definierten Endzustand einer Methode herzustellen, bevor diese verlassen wird. Hierzu wird der Code, der bspw. Dateideskriptoren schließt oder Speicher freigibt, in einem Finally-Block eingeschlossen. finally { Anweisungsfolge } Finally-Blöcke werden vom Laufzeitsystem ausgeführt, ohne den Inhalt der Try-Blöcke zu beachten. Hieraus kann auch die Wichtigkeit der Finally-Blöcke abgeleitet werden. Da derartige Blöcke auf jeden Fall ausgeführt werden, ergeben sich die folgenden Vorteile: Vermeidung von Code-Verdoppelungen . Ohne Finally-Block müsste die Anweisung zum Schließen von Dateien in jedem Catch-Block enthalten sein. Bei Verwendung von Finally hingegen wird eine Datei nur an einer Stelle im Code einer Methode geschlossen. Tritt eine unerwartete Exception auf, so wird eine Methode in einem definierten Endzustand verlassen. Anwendungsbeispiel Das folgende Beispiel stellt das Durchlaufen durch einen Vektor v dar. Zur Indizierung der Einträge wird eine feste Zahl verwendet. Es liegt auf der Hand, dass der Index des Arrays hierbei zu einer Exception führen kann, die abgefangen werden muss. Die Methode getMessage() kann dazu verwendet werden, die Aufrufreihenfolge der Methoden bis zur Auslösung der Exceptions auszugeben. try { for (int i = 0; i < groesse; i++) System.out.println("Wert: " + v.elementAt(i)); } catch (ArrayIndexOutOfBoundsException e) { System.err.println("Exception aufgetreten: " + e.getMessage()); } finally { System.out.println("Schleife wird verlassen"); } Implementierung von Exceptions Die Implementierung von benutzerdefinierten Exceptions ist durch eine Erweiterung der Klasse Exception außerordentlich einfach. Im folgenden Beispiel ist eine derartige Implementierung dargestellt, die bspw. mittels des Catch-Blocks catch (BeispielException e) {} abgefangen werden könnte. public class BeispielException extends Exception { public BeispielException () { //Verarbeitung von Aufrufen ohne Kommentar } public BeispielException (String s) { //Verarbeitung von Aufrufen mit Kommentar //Weitergabe an die Superklasse Exception super(s); } }