Vor paar Tagen nahm ich Teil an einer interessanten Diskussion im
Forum von heise.de
über die Unterschiedliche Exceptions-Philosophie in C# und Java und das Für und
Wider der Checked Exceptions.
Hier möchte ich meinen Standpunkt und die Standpunkte der anderen Teilnehmer zusammenfassen.
| Inhaltsverzeichnis |
|---|
| 1. Exceptions als Teil eines Kontraktes |
| 2. Exceptions-Kontrakte in C# |
| 3. Exceptions-Kontrakte in Java |
| 4. Behandlung von Checked Exceptions |
1. Exceptions als Teil eines KontraktesEs ist gut, wenn der Aufrufer einer Methode nicht von ihrer Implementierung sondern nur von einer klar definierten Spezifikation der Methode abhängig ist. Ein Teil einer solchen Spezifikation ist ein „Kontrakt“, in dem die Pflichten und die Vorteile sowohl für den Aufrufer als auch der aufgerufenen Methode definiert sind.
Generell gilt, dass der Aufrufer sich verpflichtet, die Methode im korrekten
Kontext und mit gültigen Parametern aufzurufen, und die Methode (bzw. alle Methoden,
die die spezifizierte Operation umsetzen - wir wollen ja polymorph bleiben :-)
sich verpflichtet korrekte Ergebnisse zu liefern bzw. ihre Aufgabe korrekt
zu erledigen. Idealerweise wird die Einhaltung solcher Kontrakte weder von dem
Aufrufer noch von der aufgerufenen Methode überprüft, sondern von der
Laufzeitumgebung der Programmiersprache selbst. (Dies ist weder in C# noch in
Java der Fall. Eine Sprache, die das kann, ist zum Beispiel Eiffel nehmen. Möchte man
Design By Contract in Java auf solche Weise betreiben, kann man sich z.B. die
aspektorientierte Erweiterung
AspectJ anschauen.)
Die Verpflichtungen des Aufrufers betreffen also den Zustand des Programmes vor dem Aufruf und den Input der Methode. Die Verpflichtungen der Methode dagegen betreffen den Zustand des Programmes nach dem Aufruf und die Ausgabe der Methode.
Die Vorbedingungen gehören zu den Verpflichtungen des Aufrufers und den Vorteilen der Methode, die Nachbedingungen gehören zu den Verprflichtungen der Methode und zu den Vorteilen des Aufrufers.
Nun, auch wenn der Aufrufer seinen Verpflichtungen nachgekommen ist, und die Methode fehlerfrei implementiert wurde, kann es passieren, dass sie ihre Aufgabe nicht erledigen kann und scheitert. Damit muss man bei jeder Methode rechnen, die Ressourcen nutzt, die außer Kontrolle des Programms stehen. Das Scheitern kann die Methode dem Aufrufer durch das Werfen einer Exception signalisieren.
Wie aber kann man die Kontrakte, die das Verhalten bei einer Exception beschreiben, formulieren und formalisieren?
Die Methode wirft eine Exception nur wenn sie scheitert und ihre Aufgabe nicht erledigen kann. Dies kann mehrere Gründe haben, die wir grundsätzlich in zwei Kategorien aufteilen können:
Es wäre ziemlich widersinnig, Kontrakte zwischen dem Aufrufer und der aufgerufenen Methode, die sich mit den Programmierfehlern befassen, zu spezifizieren. Die Kontrakte sollten uns helfen, Programmierfehler zu vermeiden, also sollten sie nie in einem fertigen Programm auftauchen. Nicht wahr? :-) Wie auch immer, einen Methode kann und braucht nicht zu versprechen, dass sie nie wegen eines Programmierfehlers scheitert. Denn einerseits ist dieses Versprechen sowieso immer implizit gegeben, anderseits kann man so einem Versprechen sowieso nie glauben.
Bei den „erwarteten“ Fehlern, sieht es anders aus. Hier könnte sich die Methode „vertraglich“ verpflichten, dass sie bestimmte Execeptions nicht melden wird. Somit ist der Aufrufer von der Notwendigkeit befreit, solche Exceptions zu behandeln.
2. Exceptions-Kontrakte in C#In C# gibt es keine Checked Exceptions. Methoden brauchen nicht zu deklarieren, welche Exceptions sie werfen könnten. Man kann das Thema Exceptions nicht zum Teil des Kontraktes zwischen dem Aufrufer und der Methode machen. Zumindest nicht mit den vorhandenen Mitteln der Sprache C# selbst. Dies ist aber nicht besonders problematisch, man kann in C# (wie auch in Java) z.B. die Wertebereiche der Parameter der Methode zum Teil des Kontraktes auch nicht machen. Man kann in C# überhaupt kaum die Kontrakte formell spezifizieren. Da verwundert es nicht, dass es bei den Exceptions auch nicht geht :-)
Damit kann man diese Thema für C# eigentlich abschließen, was ich hiermit tue.
3. Exceptions-Kontrakte in Java
In Java teilt man die Exceptions in zwei Kategorien: die checked
und die unchecked Exceptions.
Die unchecked Exceptions verhalten sich ähnlich wie die in C#, eine Methode braucht
nicht zu deklarieren, dass sie unchecked Exceptions wirft (oder durchlässt) und sie
kann nicht versprechen, dass sie es nicht tut. Die checked Exceptions hingegen
dürfen von einer Methode weder geworfen, noch durchgelassen werden, wenn die Methode
dies nicht in ihrer throws-Klausel deklariert.
Die Klassenstruktur der Exceptions in Java ist etwas verworren:

Die Klasse Throwable ist die Oberklasse aller möglichen Exceptions,
sie ist checked. Die Klasse Error sollte nur dann geworfen werden,
wenn das Programm feststellt, dass es sich in einem inkonsistentem Zustand befindet;
Es ist also die Klasse der Exceptions, die geworfen werden sollten, wenn ein
Programmierfehler festgestellt worden ist. (Eine Ausnahme ist die Klasse
TrhreadDeath)
Error und alle ihre Unterklassen sind logischerweise unchecked.
Dann gibt es die Klasse Exception. Sie und alle ihre Unterklassen,
bis auf eine eine sind checked. Die Ausnahme unter den Exceptions
(Ein Kleiner Sprachwitz muss erlaubt sein :-) ) ist die Klasse
RuntimeException und alle ihre Unterklassen, die unchecked sind.
Wie bei den Exceptions in C# kann man in Java Kontrakte bezüglich der unchecked Exceptions nicht formulieren. Eine Methode kann nicht mit den Sprachmitteln von Java versprechen, dass sie bestimmte unchecked Exceptions nicht wirft.
Hier sieht die Situation ganz anders aus. Eine Methode kann sehr wohl versprechen,
dass sie bestimmte checked Exceptions nicht wirft. Sie tut es,
indem sie diese Exceptions (oder ihre Oberklassen) nicht in ihrer
throws-Klausel angibt. Will oder kann eine Methode so eine Verpflichtung
nicht übernehmen, muss sie alle checked Exception-Klassen, die sie werfen dürfen möchte,
in der throws-Klausel aufzählen.
Oft höre ich, dass der Kontrakt den Aufrufer zur Behandlung der checked Exceptions verpflichtet. Diese Sichtweise halte ich für falsch: Die Exceptions betreffen den Ausgang der Methode, und für den Ausgang ist die Methode selbst, nicht der Aufrufer verantwortlich. Daher kann in dem Kontrakt der Aufrufer zu nichts, was den Ausgang betrifft verpflichtet werden.
Es stimmt aber, dass der Aufrufer eine checked Exception entweder fangen oder selbst
in seiner throws-Klausel deklarieren muss. Diese Notwendigkeit ergibt sich
aber nicht aus dem Kontrakt den der Aufrufer mit der Methode hat, sondern aus dem
Kontrakt, den der Aufrufer mit seinen Aufrufern hat. Wenn er nämlich nicht deklariert,
dass er eine checked Exception wirft, und er selbst Methoden aufruft, die eine solche
werfen dürfen, erkennt der schlaue Java-Compiler, dass er zu wenig tut, um seinen
eigenen Verpflichtungen nachzukommen.
4. Behandlung von Checked ExceptionsWie kann der Aufrufer damit umgehen, dass eine Methode, die er selbst aufruft nicht verspricht eine checked Exception nicht zu werfen? Er hat mehrere Möglichkeiten:
ErrorRuntimeException.Schauen wir uns die verschiedenen Optionen im Detail an: