Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger. 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 7 Ausnahmen müssen sein
Pfeil 7.1 Problembereiche einzäunen
Pfeil 7.1.1 Exceptions in Java mit try und catch
Pfeil 7.1.2 Eine NumberFormatException auffangen
Pfeil 7.1.3 Eigenschaften vom Exception-Objekt
Pfeil 7.1.4 Wiederholung abgebrochener Bereiche *
Pfeil 7.1.5 Mehrere Ausnahmen auffangen
Pfeil 7.1.6 Ablauf einer Ausnahmesituation
Pfeil 7.1.7 throws im Methodenkopf angeben
Pfeil 7.1.8 Abschlussbehandlung mit finally
Pfeil 7.2 RuntimeException muss nicht aufgefangen werden
Pfeil 7.2.1 Beispiele für RuntimeException-Klassen
Pfeil 7.2.2 Kann man abfangen, muss man aber nicht
Pfeil 7.3 Die Klassenhierarchie der Fehler
Pfeil 7.3.1 Die Exception-Hierarchie
Pfeil 7.3.2 Oberausnahmen auffangen
Pfeil 7.3.3 Schon gefangen?
Pfeil 7.3.4 Alles geht als Exception durch
Pfeil 7.3.5 Zusammenfassen gleicher catch-Blöcke mit dem multi-catch
Pfeil 7.4 Harte Fehler – Error *
Pfeil 7.5 Auslösen eigener Exceptions
Pfeil 7.5.1 Mit throw Ausnahmen auslösen
Pfeil 7.5.2 Vorhandene Runtime-Fehlertypen kennen und nutzen
Pfeil 7.5.3 Parameter testen und gute Fehlermeldungen
Pfeil 7.5.4 Neue Exception-Klassen deklarieren
Pfeil 7.5.5 Eigene Ausnahmen als Unterklassen von Exception oder RuntimeException?
Pfeil 7.5.6 Ausnahmen abfangen und weiterleiten *
Pfeil 7.5.7 Aufruf-Stack von Ausnahmen verändern *
Pfeil 7.5.8 Präzises rethrow *
Pfeil 7.5.9 Geschachtelte Ausnahmen *
Pfeil 7.6 Automatisches Ressourcen-Management (try mit Ressourcen)
Pfeil 7.6.1 try mit Ressourcen
Pfeil 7.6.2 Die Schnittstelle AutoCloseable
Pfeil 7.6.3 Mehrere Ressourcen nutzen
Pfeil 7.6.4 try mit Ressourcen auf null-Ressourcen
Pfeil 7.6.5 Unterdrückte Ausnahmen *
Pfeil 7.7 Besonderheiten bei der Ausnahmebehandlung *
Pfeil 7.7.1 Rückgabewerte bei ausgelösten Ausnahmen
Pfeil 7.7.2 Ausnahmen und Rückgaben verschwinden – das Duo return und finally
Pfeil 7.7.3 throws bei überschriebenen Methoden
Pfeil 7.7.4 Nicht erreichbare catch-Klauseln
Pfeil 7.8 Den Stack-Trace erfragen *
Pfeil 7.8.1 StackTraceElement
Pfeil 7.8.2 printStackTrace(…)
Pfeil 7.8.3 StackTraceElement vom Thread erfragen
Pfeil 7.9 Assertions *
Pfeil 7.9.1 Assertions in eigenen Programmen nutzen
Pfeil 7.9.2 Assertions aktivieren
Pfeil 7.10 Zum Weiterlesen
 

Zum Seitenanfang

7.5Auslösen eigener Exceptions Zur vorigen ÜberschriftZur nächsten Überschrift

Bisher wurden Exceptions lediglich aufgefangen, aber noch nicht selbst erzeugt. In diesem Abschnitt wollen wir sehen, wie eigene Ausnahmen ausgelöst werden. Das kann zum einen erfolgen, wenn die JVM provoziert wird, etwa bei einer ganzzahligen Division durch 0 oder explizit durch throw.

 

Zum Seitenanfang

7.5.1Mit throw Ausnahmen auslösen Zur vorigen ÜberschriftZur nächsten Überschrift

Soll eine Methode oder ein Konstruktor selbst eine Exception auslösen, muss zunächst ein Exception-Objekt erzeugt und dann die Ausnahmebehandlung angestoßen werden. Im Sprachschatz dient das Schlüsselwort throw dazu, eine Ausnahme zu signalisieren und die Abarbeitung an der Stelle zu beenden.

Als Exception-Typ soll im folgenden Beispiel IllegalArgumentException dienen, das ein fehlerhaftes Argument anzeigt:

Listing 7.16com/tutego/insel/exceptions/v1/Player.java, Konstruktor

Player( int age ) {

if ( age <= 0 )

throw new IllegalArgumentException( "Kein Alter <= 0 erlaubt!" );



this.age = age;

}

Wir sehen im Beispiel, dass negative Alter-Übergaben oder solche mit Null nicht gestattet sind und zu einem Fehler führen. Im ersten Schritt baut dazu new das Exception-Objekt über einen parametrisierten Konstruktor auf. Die Klasse IllegalArgumentException bietet einen solchen Konstruktor, der eine Zeichenkette annimmt, die den näheren Grund der Ausnahme übermittelt. Welche Parameter die einzelnen Exception-Klassen deklarieren, ist der API zu entnehmen. Nach dem Aufbau des Exception-Objekts beendet throw die lokale Abarbeitung, und die JVM sucht ein catch, das die Ausnahme behandelt.

[»]Hinweis

Ein throws IllegalArgumentException am Konstruktor ist in diesem Beispiel überflüssig, da IllegalArgumentException eine RuntimeException ist, die nicht über ein throws in der Methoden-Signatur angegeben werden muss.

Lassen wir ein Beispiel folgen, in dem Spieler mit einem negativen Alter initialisiert werden sollen:

Listing 7.17com/tutego/insel/exceptions/v1/Player.java, main()

try {

Player d = new Player( –100 );

System.out.println( d );

}

catch ( IllegalArgumentException e ) {

e.printStackTrace();

}

Das führt zu einer Exception, und der Stack-Trace, den printStackTrace() ausgibt, ist:

java.lang.IllegalArgumentException: Kein Alter <= 0 erlaubt!

at com.tutego.insel.exceptions.v1.Player.<init>(Player.java:10)

at com.tutego.insel.exceptions.v1.Player.main(Player.java:19)

[»]Hinweis

Löst ein Konstruktor eine Ausnahme aus, ist eine Nutzung wie die folgende problematisch:

Player p = null;

try {

p = new Player( v );

}

catch ( IllegalArgumentException e ) { }

p.getAge(); // BUMM: inline NullPointerException

Die Exception führt zu keinem Player-Objekt, wenn v negativ ist. So bleibt p mit null vorbelegt. Es folgt in der BUMM-Zeile eine NullPointerException. Der Programmcode, der das Objekt erwartet, aber vielleicht mit einer null rechnet, sollte mit in den try-Block. Doch üblicherweise stehen solche Fehler für Programmierfehler und werden nicht aufgefangen.

Da die IllegalArgumentException eine RuntimeException ist, hätte es in main() auch ohne try-catch so heißen können:

public static void main( String[] args ) {

Player d = new Player( –100 );

}

Die Runtime-Exception müsste nicht zwingend aufgefangen werden, aber der Effekt wäre, dass die Ausnahme nicht behandelt würde und das Programm abbräche.

class java.lang.IllegalArgumentException

extends RuntimeException
  • IllegalArgumentException()

    Erzeugt eine neue Ausnahme ohne genauere Fehlerangabe.

  • IllegalArgumentException(String s)

    Erzeugt ein neues Fehler-Objekt mit einer detaillierteren Fehlerangabe.

 

Zum Seitenanfang

7.5.2Vorhandene Runtime-Fehlertypen kennen und nutzen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Java-API bietet eine große Anzahl von Exception-Klassen, und so muss nicht für jeden Fall eine eigene Exception-Klasse deklariert werden. Viele Standardfälle, wie falsche Argumente oder falscher Programmstatus, decken Standard-Exception-Klassen ab.

[»]Hinweis

Entwickler sollten nie throw new Exception() oder sogar throw new Throwable() schreiben, sondern sich immer konkreter Unterklassen bedienen.

Einige Standard-Runtime-Exception-Unterklassen des java.lang-Pakets folgen nun in der Übersicht.

IllegalArgumentException

Die IllegalArgumentException zeigt an, dass ein Parameter nicht korrekt angegeben ist. Dieser Fehlertyp lässt sich somit nur bei Konstruktoren oder Methoden ausmachen, denen fehlerhafte Argumente übergeben wurden. Oft ist der Grund die Missachtung des Wertebereichs. Wenn die Werte grundsätzlich korrekt sind, darf dieser Fehlertyp nicht ausgelöst werden. Dazu folgen gleich noch ein paar mehr Details.

IllegalStateException

Objekte haben in der Regel Zustände. Gilt es, Operationen auszuführen, sind aber die Zustände nicht korrekt, so kann die Methode eine IllegalStateException auslösen und so anzeigen, dass in dem aktuellen Zustand die Operation nicht möglich ist. Wäre der Zustand korrekt, käme es nicht zu der Ausnahme. Bei statischen Methoden sollte es eine IllegalStateException nicht geben.[ 176 ](Im .NET-Framework gibt es eine vergleichbare Ausnahme, die System.InvalidOperationException. In Java trifft der Name allerdings das Problem etwas besser. )

UnsupportedOperationException

Implementieren Klassen Schnittstellen oder realisieren Klassen abstrakte Methoden von Oberklassen, so muss es immer eine Implementierung geben, auch wenn die Unterklasse die Operation eigentlich gar nicht umsetzen kann oder will. Anstatt den Rumpf der Methode nur leer zu lassen und einen potenziellen Aufrufer glauben zu lassen, die Methode führe etwas aus, sollten diese Methoden eine UnsupportedOperationException auslösen. Die API-Dokumentation kennzeichnet abstrakte Methoden, die Unterklassen vielleicht nicht realisieren wollen, als optionale Operationen.

Optionale Operationen in der Schnittstelle java.util.List

Abbildung 7.11Optionale Operationen in der Schnittstelle java.util.List

Unglücklicherweise gibt es auch eine javax.naming.OperationNotSupportedException. Doch diese sollte nicht verwendet werden. Sie ist speziell für Namensdienste vorgesehen und auch keine RuntimeException.

IndexOutOfBoundsException

Eine IndexOutOfBoundsException löst die JVM automatisch aus, wenn zum Beispiel ein Programm die Grenzen eines Arrays missachtet. Wir können diesen Ausnahmetyp selbst immer dann nutzen, wenn wir Indexzugriffe haben, etwa auf eine Zeile in einer Datei, und wenn der Index im falschen Bereich liegt. Von IndexOutOfBoundsException gibt es die Unterklassen ArrayIndexOutOfBoundsException und StringIndexOutOfBoundsException. In der Regel nutzen Programmierer diese Typen aber nicht. Inkonsistenzen gibt es beim Einsatz von IllegalArgumentException und IndexOutOfBoundsException. Ist etwa der Index falsch, so entscheiden sich einige Autoren für den ersten Fehlertyp, andere für den zweiten. Beides ist prinzipiell gültig. Die IndexOutOfBoundsException ist aber konkreter und zeigt eher ein Implementierungsdetail an.

Eigene NullPointerException auslösen?

Eine NullPointerException gehört mit zu den häufigsten Ausnahmen. Die JVM löst diesen Fehler etwa bei folgendem Programmstück aus:

String s = null;

s.length(); // inline NullPointerException

Eine NullPointerException zeigt immer einen Programmierfehler in einem Stück Code an, und so hat es in der Regel keinen Sinn, diesen Fehler abzufragen – der Programmierfehler muss behoben werden. Ein Programmierer löst eine NullPointerException selten selbst aus, sondern das macht die JVM automatisch. Sie kann jedoch vom Entwickler bewusst ausgelöst werden, wenn eine zusätzliche Nachricht Klarheit verschaffen soll, oder früh beim Prüfen von Parametern am Kopf einer Methode.

Oft gibt es diese NullPointerException, wenn an Methoden null-Werte übergeben wurden. Hier muss aus der API-Dokumentation klar hervorgehen, ob null als Argument erlaubt ist oder nicht. Wenn nicht, ist es völlig in Ordnung, wenn die Methode eine NullPointerException auslöst, wenn fälschlicherweise doch null übergeben wurde. Auf null zu prüfen, um dann zum Beispiel eine IllegalArgumentException auszulösen, ist eigentlich nicht nötig. Allerdings gilt auch hier, dass eine IllegalArgumentException allgemeiner und weniger implementierungsspezifisch als eine NullPointerException ist.

[»]Hinweis

Um eine NullPointerException auszulösen, ist statt throw new NullPointerException(); auch einfach ein throw null; möglich. Doch da eine selbst aufgebaute NullPointerException ohne zusätzliche Fehlernachricht selten ist, ist dieses Idiom nicht wirklich nützlich.

 

Zum Seitenanfang

7.5.3Parameter testen und gute Fehlermeldungen Zur vorigen ÜberschriftZur nächsten Überschrift

Eine IllegalArgumentException ist eine wertvolle Ausnahme, die einen internen Fehler anzeigt: dass nämlich eine Methode mit falschen Argumenten aufgerufen wurde. Eine Methode sollte im Idealfall alle Parameter auf ihren korrekten Wertebereich hin prüfen und nach dem Fail-fast-Verfahren arbeiten, also so schnell wie möglich einen Fehler melden, anstatt Fehler zu ignorieren oder zu verschleppen. Wenn etwa das Alter einer Person bei setAge(int) nicht negativ sein kann, ist eine IllegalArgumentException eine gute Wahl. Wenn der Exception-String dann noch aussagekräftig ist, hilft das bei der Behebung des Fehlers ungemein: Der Tipp ist hier, eine aussagekräftige Meldung anzugeben.

[zB]Negativbeispiel

Ist der Wertebereich beim Bilden eines Teil-Strings falsch oder ist der Index für einen Array-Zugriff zu groß, hagelt es eine StringIndexOutOfBoundsException bzw. ArrayIndexOutOfBoundsException:

System.out.println( "Orakel-Paul".substring( 0, 20 ) ); // inline

liefert:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException:¿

String index out of range: 20

Und

System.out.println( "Orakel-Paul".toCharArray()[20] ); // inline

liefert:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 20

Eine wichtige Information fehlt allerdings: Wie groß ist denn der String bzw. das Array? Java-Entwickler warten seit über zehn Jahren auf diese Information.

Da das Testen von Parametern in eine große if-throws-Orgie ausarten kann, ist es eine gute Idee, eine Hilfsklasse mit statischen Methoden wie isNull(…), isFalse(…), isInRange(…) einzuführen, die dann eine IllegalArgumentException auslösen, wenn eben der Parameter nicht korrekt ist.[ 177 ](Die müssen wir nicht selbst schreiben, da die Open-Source-Landschaft bereits mit der Klasse org.apache. commons.lang.Validate aus den Apache Commons Lang (https://commons.apache.org/proper/commons-lang/) oder mit com.google.common.base.Preconditions von Google Guava (https://github.com/google/guava) Vergleichbares bietet; in jedem Fall ist eine gute Parameterprüfung bei öffentlichen Methoden von Bibliotheken ein Muss. )

null-Prüfungen

Für null-Prüfungen bietet sich die Methode Objects.requireNonNull(reference) an, die immer dann eine IllegalArgumentException auslöst, wenn reference gleich null ist. Optional als zweites Argument lässt sich die Fehlermeldung angeben.

Tool-Unterstützung

Ein anderer Ansatz sind Prüfungen durch externe Codeprüfungsprogramme. Google zum Beispiel setzt in seinen vielen Java-Bibliotheken auf Parameter-Annotationen wie @Nonnull oder @Nullable.[ 178 ](Sie wurden in JSR-305, »Annotations for Software Defect Detection«, definiert. Java 7 sollte dies ursprünglich unterstützen, doch das wurde gestrichen. ) Statische Analysetools wie FindBugs (http://findbugs.sourceforge.net/) testen dann, ob es Fälle geben kann, in denen die Methode mit null aufgerufen wird. Zur Laufzeit findet der Test jedoch nicht statt.

 

Zum Seitenanfang

7.5.4Neue Exception-Klassen deklarieren Zur vorigen ÜberschriftZur nächsten Überschrift

Eigene Ausnahmen sind immer direkte (oder indirekte) Unterklassen von Exception (sie können auch Unterklassen von Throwable sein, aber das ist unüblich). Eigene Exception-Klassen bieten in der Regel zwei Konstruktoren: einen Standard-Konstruktor und einen mit einem String parametrisierten Konstruktor, um eine Fehlermeldung anzunehmen und zu speichern.

Um für die Klasse Player im letzten Beispiel einen neuen Fehlertyp zu deklarieren, erweitern wir RuntimeException zur PlayerException:

UML-Diagramm für PlayerException

Abbildung 7.12UML-Diagramm für PlayerException

Listing 7.18com/tutego/insel/exception/v2/PlayerException.java

package com.tutego.insel.exception.v2;



public class PlayerException extends RuntimeException {



public PlayerException() { }



public PlayerException( String s ) {

super( s );

}

}

Nehmen wir uns die Initialisierung mit dem Alter noch einmal vor. Statt der IllegalArgumentException löst der Konstruktor im Fehlerfall unsere speziellere PlayerException aus:

Listing 7.19com/tutego/insel/exception/v2/Player.java, Ausschnitt

if ( age <= 0 )

throw new PlayerException( "Kein Alter <= 0 erlaubt!" );

Im Hauptprogramm können wir auf die PlayerException reagieren, indem wir die Ausnahme explizit mit try-catch auffangen oder an den Aufrufer weitergeben – unsere Exception ist ja eine RuntimeException und müsste nicht direkt abgefangen werden:

Exception in thread "main" c.t.i.e.v2.PlayerException: Kein Alter <= 0 erlaubt!

at com.tutego.insel.exceptions.v2.Player.<init>(Player.java:10)

at com.tutego.insel.exceptions.v2.Player.main(Player.java:19)

[+]Tipp

Es ist immer eine gute Idee, Unterklassen von Exception zu bauen. Würden wir keine Unterklassen anlegen, sondern direkt mit throw new Exception() einen Fehler anzeigen, so könnten wir unseren Fehler später nicht mehr von anderen Fehlern unterscheiden. Mit der Hierarchiebildung werden nämlich die Spezialisierung bei mehreren catch-Anweisungen sowie eine Unterscheidung mit instanceof unterstützt. Sind die Fehler exakt Exception-Objekte, müssten wir unsere Fehler immer mit catch(Exception e) auffangen und bekämen so alle anderen Fehler mit aufgefangen, die dann nicht mehr unterschieden werden könnten. Allerdings sollten Entwickler nicht zu inflationär mit den Ausnahmenhierarchien umgehen; in vielen Fällen reicht eine Standardausnahme aus.

 

Zum Seitenanfang

7.5.5Eigene Ausnahmen als Unterklassen von Exception oder RuntimeException? Zur vorigen ÜberschriftZur nächsten Überschrift

Java steht mit der Ausnahmebehandlung über Exceptions nicht allein. Alle modernen Programmiersprachen verfügen über diese Sprachmittel. Allerdings gibt es eine Sache, die Java besonders macht: Die Unterscheidung zwischen geprüften und ungeprüften Ausnahmen. Daher stellt sich beim Design von eigenen Ausnahmenklassen die Frage, ob sie eine Unterklasse von RuntimeException sein sollen oder nicht. Einige Entscheidungshilfen:

  • Betrachten wir, wie die Java-API geprüfte und ungeprüfte Ausnahmen einsetzt. Die ungeprüften Ausnahmen signalisieren Programmierfehler, die es zu beheben gilt. Ein gutes Beispiel ist eine NullPointerException, ClassCastException oder ArrayIndexOutOfBoundsException. Es steht außer Frage, dass Fehler dieser Art Programmierfehler sind und behoben werden müssen. Ein catch wäre unnötig, da die Fehler ja im korrekten Code gar nicht auftreten können. Anders ist es bei geprüften Ausnahmen. Die Ausnahmen zeigen Fehler an, die unter gewissen Umständen einfach auftreten können. Eine IOException ist nicht schlimmer, denn die Datei kann nun einmal nicht vorhanden sein. Wir sollten uns bei dieser Unterscheidung aber bewusst sein, dass die JVM die Fehler von sich aus auslöst und nicht eine Methode.

  • Soll sich die Anwendung von dem Fehler »erholen« können oder nicht? Kommt es wegen einer RuntimeException zu einem Programmfehler, dann sollte die Anwendung zwar nicht »abstürzen«, allerdings ist ein sinnvolles Weiterarbeiten kaum möglich. Bei geprüften Ausnahmen ist das anders. Sie signalisieren, dass der Fehler behoben und das Programm dann normal fortgesetzt werden kann.

  • Ein Modul kann intern mit RuntimeExceptions arbeiten, und der API-Designer auf der anderen Seite, der Schnittstellen zu Systemen modelliert, kann gut auf geprüfte Ausnahmen zurückgreifen. Das ist einer der Gründe, warum moderne Frameworks wie EJB 3 oder auch Spring fast ausschließlich auf eine RuntimeException setzen: Wenn es einen Fehler gibt, dann lässt sich schwer etwas behandeln und einfach korrigieren. Zeigt etwa ein internes Modul beim Datenbankzugriff einen Fehler an, muss die ganze Operation abgebrochen werden, und nichts ist zu retten. Hier gilt im Großen, was auch bei der NullPointerException im Kleinen passiert: Der Fehler ist ein echtes Problem, und das Programm kann nicht einfach fortgeführt werden.

  • Geprüfte Ausnahmen können melden, wenn sich der Aufrufer nicht an die Vereinbarung der Methode hält. Die FileNotFoundException ist so ein Beispiel.[ 179 ](Eine Reaktion wie »File not found. Should I fake it? (Y/N)« ist auch nicht so clever. ) Hätte das Programm mit der Files-Methode exists(…) vorher nach der Existenz der Datei gefragt, wäre uns diese Ausnahme erspart geblieben. (Der Sonderfall, dass die Datei beim Test noch da war, dann aber im Hintergrund gelöscht wird, ist auch nicht zu vernachlässigen.) Der Aufrufer ist sozusagen selbst schuld, dass er eine geprüfte Ausnahme bekommt, da er die Rahmenbedingungen nicht einhält. Bei einer ungeprüften Ausnahme ist nicht der Aufrufer an dem Problem schuld, sondern ein Programmierfehler. Da geprüfte Ausnahmen in der Java-Dokumentation auftauchen, ist dem Entwickler klar, was passieren wird, wenn er die Vorbedingungen der Methode nicht einhält. Nach dieser Philosophie müsste eigentlich die NumberFormatException eine geprüfte Ausnahme sein, die Integer.parseInt(…) auslöst. Denn der Entwickler hat ja die Methode parseInt(…) mit einem falschen Wert gefüttert, also den Methodenvertrag verletzt. Eine geprüfte Ausnahme wäre nach dieser Philosophie richtig. Auf der anderen Seite lässt sich argumentieren, dass das Missachten von korrekten Parametern ein interner Fehler ist, denn es ist Aufgabe des Aufrufers, das sicherzustellen, und so kann die parseInt(…) mit einer RuntimeException aussteigen.

  • Die Unterscheidung zwischen internen Fehlern und externen Fehlern erlaubt eine Einteilung in geprüfte und ungeprüfte Ausnahmen. Die Programmierfehler mit Ausnahmen (wie NullPointerException oder ClassCastException) lassen sich vermeiden, da wir als Programmierer unseren Quellcode kontrollieren können und die Programmfehler entfernen können. Doch bei externen Fehlern haben wir als Entwickler keine Chance. Das Netzwerk kann plötzlich zusammenbrechen und uns eine SocketException und IOException bescheren. Alles das liegt nicht in unserer Hand und kann auch durch noch so sorgsame Programmierung nicht verhindert werden. Das schwächt natürlich das Argument aus dem letzten Aufzählungspunkt ab: Es lässt sich zwar abfragen, ob eine Datei vorhanden ist, um eine FileNotFoundException abzuwehren, doch wenn die Festplatte plötzlich Feuer fängt, ist uns eine IOException gewiss, denn Java-Programme sind nicht wie folgt aufgebaut: »Frage, ob die Festplatte bereit ist, und dann lies.« Wenn der Fehler also nicht innerhalb des Programms liegt, sondern außerhalb, lassen sich geprüfte Ausnahmen verwenden.

  • Bei geprüften Ausnahmen in Methodensignaturen muss sich der Nutzer auf eine bestimmte API einstellen. Eine spätere Änderung des Ausnahmetyps ist problematisch, da alle catch-Anweisungen oder throws-Klauseln abgeändert werden müssen. RuntimeExceptions sind hier flexibler. Ändert sich bei einer agilen Programmentwicklung der Typ von geprüften Ausnahmen im Lebenslauf einer Software öfters, führt das zu vielen Änderungen, die natürlich Zeit und somit Geld kosten.

Der erste Punkt führt in der Java-API zu einigen Entscheidungen, die Entwickler quälen, aber nur konsistent sind, etwa die InterruptedException. Jedes Thread.sleep(…) zum Schlafenlegen eines Threads muss eine InterruptedException auffangen. Sie kann auftreten, wenn ein Thread von außen einen Interrupt sendet. Da das auf keinen Fall einen Fehler darstellt, ist InterruptedException eine geprüfte Ausnahme, auch wenn wir dies oft als lästig empfinden und selten auf die InterruptedException reagieren müssen. Bei einem Aufbau einer URL ist die MalformedURLException ebenfalls lästig, aber stammt die Eingabe aus einer Dialogbox, kann das Protokoll einfach falsch sein.[ 180 ](Luxuriös wäre eine Prüfung zur Compilezeit, denn wenn etwa new URL("http://tutego.de") im Code steht, so kann es die Ausnahme nicht geben. Doch von nötigen try-catch-Blöcken, je nachdem, was der Compiler statisch entscheiden kann, sind wir weit entfernt. )

Geprüfte Ausnahmen sind vielen Entwicklern lästig, was zu einem Problem führt, wenn die Ausnahmen einfach aufgefangen werden, aber nichts passiert – etwa mit einem leeren catch-Block. Der Fehler sollte aber vielleicht nach oben laufen. Das Problem besteht bei einer RuntimeException seltener, da sie in der Regel an der richtigen zentralen Stelle behandelt wird.

Wenn wir die Punkte genauer betrachten, dann wird schnell eine andere Tatsache klar, sodass heute eine große Unsicherheit über die richtige Exception-Basisklasse besteht. Zwar zwingt uns der Compiler, eine geprüfte Ausnahme zu behandeln, aber nichts spricht dagegen, das bei einer ungeprüften Ausnahme ebenfalls zu tun. Integer.parseInt(…) und NumberFormatException sind ein gutes Beispiel: Der Compiler zwingt uns nicht zu einem Test, wir machen ihn aber trotzdem. Sind Entwickler konsequent und prüfen sie Ausnahmen selbstständig, braucht der Compiler den Test prinzipiell nicht zu machen. Daher folgen einige Entwickler einer radikalen Strategie und entwerfen alle Ausnahmen als RuntimeException. Die Unterscheidung, ob sich eine Anwendung dann »erholen« soll oder nicht, liegt beim Betrachter und ist nur noch reine Konvention. Mit dieser Alles-ist-ungeprüft-Version würde dann Java gleichauf mit C#, C++, Python, Groovy … liegen.[ 181 ](Doch eines ist sicher: Java-Vater James Gosling ist dagegen: http://www.artima.com/intv/solid.html. )

Besondere Rückgaben oder Ausnahmen?

Nicht immer ist eine Ausnahme nötig, doch wann es eine Rückgabe wie null oder -1 gibt und wann eine Ausnahme ausgelöst werden soll, ist nicht immer einfach zu beantworten und hängt vom Kontext ab. Ein Beispiel: Eine Methode liest eine Datei ein und führt eine Suche durch. Wenn eine bestimmte Teilzeichenkette nicht vorhanden ist, soll die Methode dann eine Ausnahme werfen oder nicht? Hier kommt es darauf an:

  1. Wenn das Dokument in der ersten Zeile eine Kennung tragen muss und der Test prüft auf diese Kennung, dann liegt ein Protokollfehler vor, wenn diese Kennung nicht vorhanden ist.

  2. Im Dokument gibt es eine einfache Textsuche. Ein Suchwort kann enthalten sein, muss aber nicht.

Im ersten Fall passt eine Ausnahme gut, da ein interner Fehler vorliegt. Muss die Kennung in der Datei sein, ist sie es aber nicht, darf dieser Fehler nicht untergehen, und eine Ausnahme zeigt das perfekt an. Ob geprüft oder ungeprüft, steht auf einem anderen Blatt. Im zweiten Fall ist eine Ausnahme unangebracht, da es kein Fehler ist, wenn der Such-String nicht im Dokument ist; das kann vorkommen. Das ist das Gleiche wie bei indexOf(…) oder matches(…) von String – die Methoden würden ja auch keine Ausnahmen werfen, wenn es keine Übereinstimmung gibt.

 

Zum Seitenanfang

7.5.6Ausnahmen abfangen und weiterleiten * Zur vorigen ÜberschriftZur nächsten Überschrift

Die Ausnahme, die ein catch-Block auffängt, kann mit einem throw wieder neu ausgelöst werden – das nennt sich rethrow. Ein Beispiel soll die Arbeitsweise verdeutlichen. Eine Hilfsmethode createUriFromHost(String) setzt vor einen Hostnamen "http://" und liefert das Ergebnis als URI-Objekt zurück. createUriFromHost("tutego.de") liefert somit einen URI mit http://tutego.de. Ist der Hostname aber falsch, löst der Konstruktor der URI-Klasse eine Ausnahme aus:

Listing 7.20Rethrow.java

import java.net.*;



public class Rethrow {



public static URI createUriFromHost( String host ) throws URISyntaxException {

try {

return new URI( "http://" + host );

}

catch ( URISyntaxException e ) {

System.err.println( "Hilfe! " + e.getMessage() );



throw e;

}

}



public static void main( String[] args ) {

try {

createUriFromHost( "tutego.de" );

createUriFromHost( "%" );

}

catch ( URISyntaxException e ) {

e.printStackTrace();

}

}

}

Die Klasse URI testet die Strings genauer als die URL-Klasse, sodass wir in diesem Beispiel URI nutzen. Die Ausnahmen im Fehlerfall sind auch etwas anders; URISyntaxException ist die Ausnahme bei URI; MalformedURLException ist die Ausnahme bei URL. Genau diesen Fehler provozieren wir, indem wir dem Konstruktor ein "http://%" übergeben, was ein offensichtlich falscher URI ist. Unsere Methode wird die URISyntaxException auffangen, den Fehler auf der Standardfehlerausgabe melden und dann weiterleiten, denn wirklich behandeln kann unsere Methode das Problem nicht; sie kann nur melden, was ein Vorteil ist, wenn der Aufrufer dies nicht tut.

Die Programmausgabe ist:

Hilfe! Malformed escape pair at index 7: http://%

java.net.URISyntaxException: Malformed escape pair at index 7: http://%



at java.net.URI.<init>(URI.java:595)

at Rethrow.createUriFromHost(Rethrow.java:7)

at Rethrow.main(Rethrow.java:18)
 

Zum Seitenanfang

7.5.7Aufruf-Stack von Ausnahmen verändern * Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn wir in einer Ausnahmebehandlung eine Exception e auffangen und genau diese dann mit throw e weiterleiten, müssen wir uns bewusst sein, dass die Ausnahme e auch den Aufruf-Stack weitergibt. Aus dem vorangehenden Beispiel:

java.net.URISyntaxException: Malformed escape pair at index 7: http://%

at java.net.URI$Parser.fail(URI.java:2829)

at java.net.URI$Parser.scanEscape(URI.java:2957)



at java.net.URI.<init>(URI.java:595)

at Rethrow.createUriFromHost(Rethrow.java:7)

at Rethrow.main(Rethrow.java:18)

Die main(…)-Methode fängt den Fehler von createUriFromHost(…) ab, aber diese Methode steht nicht ganz oben im Aufruf-Stack. Die Ausnahme stammte ja gar nicht von createUriFromHost(…) selbst, sondern von fail(…), sodass fail(…) oben steht. Ist das nicht gewünscht, kann es korrigiert werden, denn die Basisklasse für alle Ausnahmen Throwable bietet die Methode fillInStackTrace(), mit der sich der Aufruf-Stack neu füllen lässt. Unsere bekannte Methode createUriFromHost(…) soll auf fillInStackTrace() zurückgreifen:

Listing 7.21RethrowWithFillInStackTrace.java, createUriFromHost()

public static URI createUriFromHost( String host ) throws URISyntaxException {

try {

return new URI( "http://" + host );

}

catch ( URISyntaxException e ) {

System.err.println( "Hilfe! " + e.getMessage() );

e.fillInStackTrace();

throw e;

}

}

Kommt es in createUriFromHost(…) zur URISyntaxException, so fängt unsere Methode diese ab. Ursprünglich ist in e der Aufruf-Stack mit der fail(…)-Methode ganz oben gespeichert, allerdings löscht fillInStackTrace() zunächst den ganzen Stack-Trace und füllt ihn neu mit dem Pfad, den der aktuelle Thread zu der Methode führt, die fillInStackTrace() aufruft – das ist createUriFromHost(…). Daher beginnt die Konsolenausgabe auch mit unserer Methode:

Hilfe! Malformed escape pair at index 7: http://%

java.net.URISyntaxException: Malformed escape pair at index 7: http://%

at RethrowWithFillInStackTrace.createUriFromHost(

RethrowWithFillInStackTrace.java:11)


at RethrowWithFillInStackTrace.main(RethrowWithFillInStackTrace.java:19)
 

Zum Seitenanfang

7.5.8Präzises rethrow * Zur vorigen ÜberschriftZur nächsten Überschrift

Die Notwendigkeit, Ausnahmen über einen Basistyp zu fangen, ist mit dem Einzug vom multi-catch gesunken. Doch für gewisse Programmteile ist es immer noch praktisch, alle Fehler eines gewissen Typs aufzufangen. Wir können auch so weit in der Ausnahmehierarchie nach oben laufen, um alle Fehler aufzufangen – dann haben wir es mit einem try {} catch( Throwable t){} zu tun. Ein multi-catch ist für geprüfte Ausnahmen besonders gut, aber bei ungeprüften Ausnahmen ist eben nicht immer klar, was als Fehler denn so ausgelöst wird, und ein catch(Throwable t) hat den Vorteil, dass es alles wegfischt.

Problemstellung

Werden Ausnahmen über einen Basistyp gefangen und wird diese Ausnahme mit throw weitergeleitet, dann ist es naheliegend, dass der aufgefangene Typ genau der Typ ist, der auch bei throws in der Methodensignatur stehen muss.

Stellen wir uns vor, ein Programmblock nimmt einen Screenshot und speichert ihn in einer Datei. Kommt es beim Abspeichern zu einem Fehler, soll das, was vielleicht schon in die Datei geschrieben wurde, gelöscht werden; die Regel ist also: Entweder steht der Screenshot komplett in der Datei, oder es gibt gar keine Datei. Die Methode kann so aussehen, wobei sie Ausnahmen an den Aufrufer weitergibt:

Listing 7.22RethrowTypes.java, saveScreenshot()

public static void saveScreenshot( String filename )

throws AWTException, IOException {

try {

Rectangle r = new Rectangle( Toolkit.getDefaultToolkit().getScreenSize() );

BufferedImage screenshot = new Robot().createScreenCapture( r );

ImageIO.write( screenshot, "png", new File( filename ) );

}

catch ( AWTException e ) {

throw e;

}

catch ( IOException e ) {

Files.delete( Paths.get( filename ) );

throw e;

}

}

Mit den beiden catch-Blöcken sind wir genau auf die Ausnahmen eingegangen, die createScreenCapture(Rectangle) und write(…) auslösen. Das ist richtig, aber löschen wir wirklich immer die Dateireste, wenn es Probleme beim Schreiben gibt? Richtig ist, dass wir immer dann die Datei löschen, wenn es zu einer IOException kommt. Aber was passiert, wenn die Implementierung eine RuntimeException auslöst? Dann wird die Datei nicht gelöscht, aber das ist gefragt! Das scheint einfach gefixt, denn statt

catch ( IOException e ) {

// Datei löschen

throw e;

}

schreiben wir:

catch ( Throwable e ) {

// Datei löschen

throw e;

}

Doch können wir das Problem so lösen? Der Typ Throwable passt doch gar nicht mehr mit dem deklarierten Typ IOException in der Methodensignatur zusammen:

Listing 7.23RethrowTypes2.java, Ausschnitt

public static void saveScreenshot( String filename )

throws AWTException /*1*/, IOException /*2*/ {



catch ( AWTException /*1*/ e ) {

throw e;

}

catch ( Throwable /*?*/ e ) {

Files.delete( Paths.get( filename ) );

throw e;

}

}

Die erste catch-Klausel fängt die AWTException und leitet sie weiter. Damit wird saveScreenshot(String) zum möglichen Auslöser von AWTException, und die Ausnahme muss mit throws an die Signatur. Wenn nun ein catch-Block jedes Throwable auffängt und diesen Throwable-Fehler weiterleitet, ist zu erwarten, dass an der Signatur auch Throwable stehen muss und IOException nicht reicht. Das war auch bis Java 6 so, aber in Java 7 kam eine Anpassung von einer unpräziseren hin zur präziseren Typanalyse.

Präzisere Typprüfung

Seit Java 7 führt der Compiler eine präzisere Typanalyse durch: Immer dann, wenn in einem catch-Block ein throw stattfindet, ermittelt der Compiler die im try-Block tatsächlich aufgetretenen geprüften Exception-Typen und schenkt dem im catch genannten Typ für das rethrow im Prinzip keine Beachtung. Statt des gefangenen Typs wird der Compiler den durch die Codeanalyse gefundenen Typ beim rethrow melden.

Der Compiler erlaubt nur dann das präzise rethrow, wenn die catch-Variable nicht verändert wird. Zwar ist eine Veränderung einer nichtfinalen catch-Variablen wie auch unter Java 1.0 erlaubt, doch wenn die Variable belegt wird, schaltet der Compiler von der präzisen in die unpräzise Erkennung zurück. Führen wir etwa die folgende Zuweisung ein, so funktioniert das Ganze schon nicht mehr:

catch ( Throwable e ) {

// Datei löschen

e = new IllegalStateException();

throw e;

}

Die Zuweisung führt zu dem Compilerhinweis, dass jetzt auch Throwable mit in die throws-Klausel muss.

Stilfrage

Die catch-Variable kann für die präzisere Typprüfung den Modifizierer final tragen, muss das aber nicht tun. Immer dann, wenn es keine Veränderung an der Variablen gibt, wird der Compiler sie als final betrachten und eine präzisere Typprüfung durchführen – daher nennt sich das auch effektiv final. Die Java Language Specification rät vom final-Modifizierer aus Stilgründen ab. Es ist daher Quatsch, überall ein final dazuzuschreiben, um die präzisere Typprüfung zu dokumentieren.

Migrationsdetail

Da der Compiler nun mehr Typwissen hat, stellt sich die Frage, ob alter Programmode mit dem neuen präziseren Verhalten vielleicht ungültig werden könnte. Theoretisch ist das möglich, aber die Sprachdesigner haben in über 9 Millionen Zeilen Code[ 182 ](Die Zahl stammt aus der FOSDEM-Präsentation 2011 »Project Coin: Language Evolution in the Open«. ) von unterschiedlichen Projekten keine Probleme gefunden. Prinzipiell könnte der Compiler jetzt unerreichbaren Code finden, der vorher versteckt blieb. Ein kleines Beispiel, was vor Java 7 compiliert, aber ab Java 7 nicht mehr:

try {

throw new FileNotFoundException();

}

catch ( IOException e ) {

try {

throw e; // e ist für den Compiler vom Typ FileNotFoundException

}

catch ( MalformedURLException f ) { }

}

Die Variable e in catch (IOException e) ist effektiv final, und der Compiler führt die präzisere Typerkennung durch. Er findet heraus, dass der wahre rethrow-Typ nicht IOException, sondern FileNotFoundException ist. Wenn dieser Typ dann mit throw e weitergeleitet wird, kann ihn catch(MalformedURLException) nicht auffangen. Vor Java 7 war das etwas anders, denn hier wusste der Compiler nur, dass e irgendeine IOException ist, und es hätte ja durchaus die IOException-Unterklasse MalformedURLException sein können. (Warum MalformedURLException aber eine Unterklasse von IOException ist, steht auf einem ganz anderen Blatt.)

 

Zum Seitenanfang

7.5.9Geschachtelte Ausnahmen * Zur vorigen ÜberschriftZur nächsten Überschrift

Der Grund für eine Ausnahme mag der sein, dass ein eingebetteter Teil versagt. Das ist vergleichbar mit einer Transaktion: Ist ein Teil der Kette fehlerhaft, so ist der ganze Teil nicht ausführbar. Bei Ausnahmen ist das nicht anders. Nehmen wir an, wir haben eine Methode foo(), die im Falle eines Misslingens eine Ausnahme HellException auslöst. Ruft unsere Methode foo() nun ein Unterprogramm bar() auf, das zum Beispiel eine Ein-/Ausgabeoperation tätigt, und das geht schief, wird die IOException der Anlass für unsere HellException sein. Es liegt also nahe, bei der Nennung des Grunds für das eigene Versagen das Misslingen der Unteraufgabe zu nennen (wieder ein Beweis dafür, wie »menschlich« Programmieren sein kann).

Eine geschachtelte Ausnahme (engl. nested exception) speichert einen Verweis auf eine weitere Ausnahme. Wenn ein Exception-Objekt aufgebaut wird, lässt sich der Grund (engl. cause) als Argument im Konstruktor der Throwable-Klasse übergeben. Die Ausnahme-Basisklasse bietet dafür zwei Konstruktoren:

class java.lang.Throwable

implements Serializable
  • Throwable(Throwable cause)

  • Throwable(String message, Throwable cause)

Den Grund der Ausnahme erfragt die Methode Throwable getCause().

Da Konstruktoren in Java nicht vererbt werden, bieten die Unterklassen oft Konstruktoren an, um den Grund anzunehmen: Zumindest Exception macht das und kommt somit auf vier Erzeuger:

class java.lang.Exception

extends Throwable
  • Exception()

  • Exception(String message)

  • Exception(String message, Throwable cause)

  • Exception(Throwable cause)

Einige der tiefer liegenden Unterklassen haben dann auch diese Konstruktortypen mit Throwable-Parameter, wie IOException, SQLException oder ClassNotFoundException, andere wiederum nicht, wie PrinterException. Eigene Unterklassen können auch mit initCause( Throwable) genau einmal eine geschachtelte Ausnahme angeben.

Geprüfte Ausnahmen in ungeprüfte Ausnahmen verpacken

In modernen Frameworks ist die Nutzung von Ausnahmen, die nicht geprüft werden müssen, also Exemplare von RuntimeException sind, häufiger geworden. Bekannte zu prüfende Ausnahmen werden in RuntimeException-Objekte verpackt (eine Art Exception-Wrapper), die den Verweis auf die auslösende Nicht-RuntimeException speichern.

Dazu ein Beispiel. Die folgenden drei Zeilen ermitteln, ob die Webseite zu einer URL verfügbar ist:

HttpURLConnection.setFollowRedirects( false );

HttpURLConnection con = (HttpURLConnection)(new URL( url ).openConnection());

boolean available = con.getResponseCode() == HttpURLConnection.HTTP_OK;

Da der Konstruktor von URL eine MalformedURLException auslösen kann und es beim Netzwerkzugriff zu einer IOException kommen kann, müssen diese beiden Ausnahmen entweder behandelt oder an den Aufrufer weitergereicht werden (MalformedURLException ist eine spezielle IOException, das verkürzt das Programm etwas). Wir wollen eine Variante wählen, in der wir die geprüften Ausnahmen in eine RuntimeException hüllen, sodass es eine Utility-Methode gibt und sich der Aufrufer nicht lange mit irgendwelchen Ausnahmen beschäftigen muss:

Listing 7.24NestedException.java, NestedException

public static boolean isAvailable( String url ) {

try {

HttpURLConnection.setFollowRedirects( false );

HttpURLConnection con = (HttpURLConnection)(new URL( url ).openConnection());

return con.getResponseCode() == HttpURLConnection.HTTP_OK;

}

catch ( IOException e ) {

throw new RuntimeException( e );

}

}



public static void main( String[] args ) {

System.out.println( isAvailable( "http://laber.rhabar.ber/" ) ); // false

System.out.println( isAvailable( "http://www.tutego.de/" ) ); // true

System.out.println( isAvailable( "taube://sonsbeck/schlossstrasse/5/" ) ); // inline

}

In der letzten Zeile kommt es zu einer Ausnahme, da es das Protokoll »taube« nicht gibt. Die Ausgabe ist folgende:

Exception in thread "main" java.lang.RuntimeException: java.net.UnknownHostException:¿ laber.rabar.ber

at NestedException.isAvailable(NestedException.java:15)

at NestedException.main(NestedException.java:20)

Caused by: java.net.UnknownHostException: laber.rabar.ber

at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184)

at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)

In der Praxis ist es bei großen Stack-Traces – und einem Szenario, bei dem abgefangen und neu verpackt wird – fast unmöglich, aus der Ausgabe den Verlauf zu entschlüsseln, da sich diverse Teile wiederholen und dann wieder abgekürzt werden. Die duplizierten Teile sind zur Verdeutlichung fett hervorgehoben.

[»]Hinweis

Statt den parametrisierten Konstruktor new RuntimeException(e) zu verwenden, hätten wir auch initCause(…) verwenden können:

catch ( IOException e ) {

RuntimeException e2 = new RuntimeException();

e2.initCause( e );

throw e2;

}

Allerdings gibt es einen wichtigen Unterschied: ohne den parametrisierten Konstruktor und mit initCause(…) gibt es keinen automatischen Aufruf von fillInStackTrace(), daher sieht der Stack-Trace unterschiedlich aus.

UncheckedIOException

Fehler bei Ein-/Ausgabe-Operationen werden in Java traditionell über eine geprüfte Ausnahme vom Typ IOException gemeldet. Bei Frameworks ist das zum Teil etwas lästig, sodass es im Paket java.io eine Art Wrapper-Klasse gibt, die eine geprüfte IOException in einer ungeprüften UncheckedIOException mantelt.

class java.io.UncheckedIOException

extends RuntimeException
  • UncheckedIOException(IOException cause)

    Ummantelt cause.

  • UncheckedIOException(String message, IOException cause)

    Ummantelt cause mit einer zusätzlichen Meldung.

Bisher macht die Java-Bibliothek nur an einer Stelle von diesem Ausnahmetyp Gebrauch, und das ist bei lines() der Klasse BufferedReader, damit bei der Stream-API die geprüften Ausnahmen nicht im Weg stehen.

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück

 

 


Copyright © Rheinwerk Verlag GmbH 2017

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de