Sun hat sich bei der Entwicklung von Java 5zwei oberste Ziele gesetzt: Erstens soll die Entwicklung mit Java einfacher werden und zweitens soll dabei die Abwärtskompatibilität gewahrt bleiben. Um mit Java 1.x kompatibel zu bleiben, wurde in den Java-Compiler eine Art Präprozessor integriert, der die neuen Sprachkonstrukte aus Java 5 erkennt und in herkömmlichen Java-Code übersetzt. So wird aus der schönen neuen Foreach-Schleife wieder die gute alte For-Schleife mit Iterator oder Schleifenzähler. Die Laufzeitumgebung (JVM) kennt weiterhin nur den klassischen Bytecode ohne Generics, Autoboxing et al. Das ist der Grund für viele der Fallstricke in Java 5, die im folgenden Artikel, strukturiert nach dem Sprachkonstrukt, beschrieben werden.
Autoboxing: Der Programmierer muss wissen, was er tut
Die transparente Umwandlung von primitiven Datentypen zu ihrer Objektrepräsentation und zurück erspart hässliche Methodenkaskaden beim Zusammenspiel von algebraischen Operationen mit generischen Konstrukten wie dem Collections-Framework oder die Verwendung von unnötig mächtigen Klassen wie BigDecimal.Das Boxing, die Umwandlung eines Datentyps zu seiner Objektrepräsentation, kostet Rechenzeit und Hauptspeicherplatz: Ein neues Objekt muss erzeugt und befüllt werden. Da dies mit Autoboxing transparent erfolgt, besteht die Gefahr, dass der Entwickler nach einiger Zeit, mit zunehmender Selbstverständlichkeit des Autoboxing, das Bewusstsein für das mögliche Performanceproblem verliert. Im ersten Codefragment in Listing 1 ist klar ersichtlich, dass hier viele Objekte erzeugt werden. Das zweite Codefragment verschleiert dies jedoch durch die Verwendung des Autoboxing. Die ersten beiden Codefragmente in Listing 1 sind um ca. den Faktor zehn langsamer als das dritte Codefragment, das eine mögliche Alternative darstellt. Schlimmer als der Performanceverlust durch die Objekterzeugung ist die erhöhte Beschäftigung des Garbage Collector - besonders bei Server-Anwendungen. Es kann hier sogar vorkommen, dass der Garbage Collector bei den vielen neu erzeugten Objekten mit dem Aufräumen nicht mehr nachkommt und das gesamte System blockiert, um in Ruhe arbeiten zu können.
Listing 1
Autoboxing und Performance
final long LOOPS = 10000l;//klassisches FehlermusterList integerList = new ArrayList(LOOPS);for (int i = 0; i < LOOPS; i++){integerList.add( new Integer(i) );}//Java 5.0-FehlermusterList<Integer> integerList = new ArrayList<Integer>(LOOPS);for (int i = 0; i < LOOPS; i++){integerList.add(i); //Autoboxing}//performantere Versionint[] intArr = new int[LOOPS];for (int i = 0; i < LOOPS; i++){intArr[i] = i;}
- Hohe Anzahl der Schleifendurchläufe im gesamten Programmlauf (auch 100 Iterationen pro Schleife ist kritisch, wenn im Aufrufweg drei solcher Schleifen geschachtelt sind).
- kurze Lebensdauer der aus dem Boxing entstandenen Objekte. Dies deutet darauf hin, dass die Objektrepräsentation unter Umständen nicht wirklich benötigt wird, da sofort wieder eine Umwandlung zurückstattfindet.
- mehrfaches Boxing mit gleichen Werten. Da alle Objektrepräsentationen (Float, Integer, Double etc.) unveränderbar (immutable) sind, können Referenzen auf sie geteilt werden, ohne Seiteneffekte fürchten zu müssen. Wandelt man hingegen in einer Schleife immer wieder denselben primitiven Datenwert in ein Objekt um, so entstehen hier unnötig viele Instanzen.
Listing 2
Integer Caching
Integer intInner1 = 123;Integer intInner2 = 123;Integer intOuter1 = 500;Integer intOuter2 = 500;//Referenzvergleich im Integer-Caching-BereichSystem.out.println( intInner1 == intInner2 ); //true//Referenzvergleich außerhalb des Integer-Caching-BereichsSystem.out.println( intOuter1 == intOuter2 ); //false
Listing 3
Typkompatibilität beim Autoboxing
//Gültige Zuweisung bei primitiven Datentypenfloat fa = 123; //int zu floatlong la = 123; //int zu long//Ungültige Zuweisungen beim AutoboxingFloat fb = 123; //int zu Float (Compiler-Error)Long lb = 123; //int zu Long (Compiler-Error)//Gültige Zuweisungen beim AutoboxingFloat fc = 123f; //float zu FloatLong lc = 123l; //long zu Long//Array-Typen werden nicht automatisch umgewandeltInteger[] iarr = new int[] {1,2,3,4}; //Compiler-Error
Integer i = null;//...int i2 = i; //NullPointerException!
Diese Eigenheit ist freilich eine Grauzone in der Theorie des Autoboxing, die z.B. C# auch erst ab Version 2.0 mit den Nullable Types in den Griff bekommt. Gleichzeitig sind alle anderen beschriebenen Fallstricke des Java 5 Autoboxing in C# nicht zu finden. Zusammenfassend zu den Fallstricken des Autoboxing lässt sich sagen, dass die beschriebenen Fehlermuster recht einfach durch die automatisierte, statische Codeanalyse gefunden werden können. Es ist zu hoffen, dass die hierfür gängigen Werkzeuge wie FindBugs [4] diese Fehlermuster bald erkennen werden.
Generics: Sackgassen und Irrwege
Die Generics sind in Java 5 die große Ausnahme gegenüber der neuen Leichtigkeit des Seins. Mittlerweile ist viel und gute Literatur vorhanden, um sich in das Thema der Generics einzuarbeiten [5] [6] [7]. In der Praxis ergeben sich aber trotzdem immer wieder Probleme durch die Komplexität dieses Sprachmittels. Wie der folgende Abschnitt zeigt, wird durch die Generics der Code nicht unbedingt typsicherer, generischer und leichter verständlich.Die Daseinsberechtigung der Generics ist die erhöhte Typsicherheit. Diese kann aber leicht durch die Raw Types ausgehebelt werden, die aus Kompatibilitätsgründen eingeführt wurden. Raw Types sind generische Klassen, die ohne Typ-Parameter verwendet werden, also an keinen Typen gebunden werden. Da die Laufzeitumgebung keine Generics kennt, können durch Zuweisung an seinen Raw Type einem streng typisierten Objekt ungültige Typen untergeschoben werden (Listing 4, Abb. 1). Die Java-Sprachspezifikation [8] erlaubt es, dass zukünftige Java-Versionen die Raw Types nicht mehr unterstützen.
Listing 4
Raw Types hebeln die Typsicherheit aus
//Raw Type von List wird verwendetList wines = new ArrayList();wines.add( new Wine("Cabernet Sauvignon") );wines.add( new Beer("Chiemseer Braustoff") ); //erlaubt aber falsch//Zuweisung eines Raw Type zu einem generischen Typen//Erlaubt, aber Compiler-Warnung ?unchecked assignment?List<Wine> wines2 = wines;Wine wine1 = wines2.get(0);Wine wine2 = wines2.get(1); //ClassCastException!
Listing 5
Typ-Kontrollen an den Grenzen
public List<Wine> deliverWine(List<Wine> wines){//Vorbedingung prüfenAssert.checkCollection(wines, Wine.class);//...//Liste dynamisch typsicher machenreturn Collections.checkedList(wines, Wine.class);}//Legacy Code -----------------------List wines = new ArrayList();wines.add(new Wine("Grauburgunder"));List delivered = deliverWine(wines);//Hier ClassCastException, obwohl Raw Type (vgl. Listing 4)delivered.add( new Beer("Wolpertinger Urstoff") );
Darum sollte in Java 5-Code auf die Verwendung von Raw Types verzichtet werden - generische Klassen sollten immer parametrisiert verwendet werden. Bei der Anbindung von Java 1.x-Code kommt man aber unter Umständen nicht um den Kontakt mit Raw Types herum. Hier muss man sich abschotten. Listing 5 zeigt, wie man sich in seiner heilen, typsicheren Welt von der bösen untypisierten Welt schützen kann (die verwendete Assert-Klasse befindet sich auf der Heft-CD). Wird eine Methode von fremdem Code aufgerufen (z.B. an den Außenschnittstellen eines Systems), dann sollten zumindest alle Collections überprüft werden, ob sie die korrekten Typen enthalten (Assert.checkCollection()). In die andere Richtung sollten zumindest alle Collections dynamisch typsicher gemacht werden, wenn sie an Legacy-Code herausgereicht werden. Die Collections-Klasse bietet hierfür die statischen Methoden checkedCollection(), checkedList() usw. Diese stülpen einen Wrapper um den jeweiligen Collection-Typ. Dieser wirft eine ClassCastException, sobald in eine Collection Elemente des falschen Typs eingefügt werden. Damit tritt die Ausnahme an der Stelle ihrer Ursache auf, dem Einfügen, und nicht erst, wenn das Element herausgenommen wird.
Ein Punkt, der Entwickler in Selbstzweifel treiben kann, ist die Typkompatibilität bei den Generics. Schon bei einem einfachen Beispiel gerät der Gedankengang ins Stocken:
List<Beer> beers = new ArrayList<Beer>();List<AlcoholicDrink> alcoDrinks;alcoDrinks = beers; //Compiler-Error: Incompatible types
Wieso kann ich einer Ansammlung an alkoholischen Getränken keine Ansammlung an Bieren zuordnen - die Listenelemente sind doch zueinander typkompatibel? Der Grund hierfür liegt daran, dass sonst folgende Zeilen möglich wären:
alcoDrinks.add( new Wine("Cabernet Sauvignon") );Beer beer = beers.get(0); //ClassCastException
Dadurch wären wieder Typ-Inkonsistenzen bzw. ClassCastExceptions möglich, obwohl durchgängig Generics verwendet wurden. Noch schlimmer wird die Logik zur Typ-Kompatibilität bei der Verwendung von Bounds bei der Typ-Parametrisierung:
List<? extends AlcoholicDrink> liAlco;liAlco.add( new Wine("Grauburgunder") ); //Compiler-Fehler: Incompatible typesliAlco.add( new AlcoholicDrink() ); //Compiler-ErrorliAlco.add( new Drink() ); //Compiler-ErrorliAlco.add( new Object() ); //Compiler-Error
Parametrisiert man einen Typen anhand von extends-Bound, so kann man einer Variable nichts mehr zuweisen, die in dem Typen parametrisiert ist (wie es der Parameter der add()-Methode ist). Der Grund dafür ist, dass sonst die Typsicherheit nicht mehr gewährleistet werden könnte. Die Begründung dafür liefert die Überlegung, welche konkreten Typen dem geboundeten Typen zugewiesen werden können: Einer List von ? extends AlcoholicDrink kann sowohl eine List von Wine als auch eine List von Beer zugeordnet werden:
List<Wine> liWine;List<Beer> liBeer;List<? extends AlcoholicDrink> liAlco = liWine;List<? extends AlcoholicDrink> liAlco = liBeer;liAlco.add( ??? );
Eine Bierliste kann aber nur Sorten oder eventuelle Unterarten, wie z.B. Pils oder Weißbier enthalten; eine Weinliste nur Weine und ihre Unterarten. Somit gibt es keine Schnittmenge an gültigen Getränken, die in beide Listen passen würden und somit auch keinen geeigneten Paramter-Typen für die add()-Methode. Auch die super-Bound verhält sich in dieser Situation wenig intuitiv:
List<? super AlcoholicDrink> liAlco;liAlco.add( new Object() ); //Compiler-Error: Incompatible typesliAlco.add( new Drink() ); //Compiler-ErrorliAlco.add( new AlcoholicDrink() );liAlco.add( new Wine("Cabernet Sauvignon") );liAlco.add( new Beer("Chiemseer Braustoff") );
Hier können nur alle Subtypen der Bound dem Parameter zugewiesen werden - was man eigentlich bei super nicht erwartet. Auf die Spur führt hier dieselbe Überlegung wie oben: Einer List von ? super AlcoholicDrink kann eine List von Object, Drink oder AlcoholicDrink zugewiesen werden. Also akzeptieren alle diese Listen mindestens einen AlcoholicDrink oder einen seiner Derivate wie z.B. Beer. Auch in die andere Richtung, dem Auslesen von parametrisierten Werten, gibt es Regeln zu beachten:
List<? extends AlcoholicDrink> liAlco;Drink drink = liAlco.get(0);AlcoholicDrink alcoDrink = liAlco.get(0);Beer beer = liAlco.get(0); //Compiler-Error: Incompatible typesList<? super AlcoholicDrink> liAlco;AlcoholicDrink alcoDrink = liAlco.get(0) ; //Compiler-Error: Incompatible typesDrink drink = liAlco.get(0); //Compiler-ErrorObject o = liAlco.get(0) ;
Aus einer List< ? extends AlcoholicDrink> kann man sich alle Supertypen von AlcoholicDrink herausholen. Aus einer List< ? super AlcoholicDrink> kann man nur Objects herausholen. Abpictureung 2 zeigt zusammenfassend die Typ-Kompatibilität bei der Verwendung von Bounds - beim Schreiben (Zuweisung an) und dem Lesen (Zuweisung von). Die kompatiblen Typen sind dabei in Wildcard-Syntax ausgedrückt. Die aufgezeigte Semantik gilt selbstverständlich nicht nur für Collections, sondern bei jeder Verwendung von Bounds bei der Typ-Parametrisierung.
Statische Importe
Der Import von statischen Elementen spart Tipparbeit. Typische Hilfsfunktionen (Math.sqrt(), LogManager.getLogManager()) und allgemeingültige Variablen (Answers.TO_THE_GREAT_QUESTION_OF_LIFE) können so referenziert werden, als seien sie der importierenden Klasse eigen. Die lästige Qualifikation der Container-Klasse entfällt. Es gibt dabei aber eine Fehlerquelle zu beachten: die Überlagerung von statischen Importen mit Methoden oder Feldern der Klasse. Hierfür ist nämlich spezifiziert, dass ein statischer Import nie eine interne Definition der Klasse überschreibt (Listing 6). Der Compiler meldet aber keinen Fehler. Dadurch kann es, besonders bei Wartungsarbeiten an einer Klasse, unklar werden, welche Methode gerade benutzt wird und falsche Annahmen getroffen werden.Listing 6
Statische Importe
import static java.lang.Math.sqrt;public class MyClass {public double sqrt(double x){ return x*x; }public void foo(){double res = sqrt(5d); //benutzt MyClass.sqrt()}}
Zusammenfassung
Java hat seine Unschuld verloren. Es muss noch viel Arbeit in Form von Java 5-Coding-Styles geleistet werden, um die neue Komplexität (durch Generics) und Fehlerpotenziale (Autoboxing, statische Importe) in den Griff zu bekommen. Nichtsdestoweniger bieten die neuen Sprachmittel in Java 5 viele Möglichkeiten, die Java-Programmierung ausdrucksstärker und einfacher zu machen.Zu guter Letzt geht noch ein Dank an Prof. Dr. Siedersleben, den Studenten und Dozenten der Fachhochschule Rosenheim, deren Seminar J2SE 5.0 [9] die Grundlage für diesen Artikel geliefert hat.
Josef Adersberger (Josef.Adersberger@QAware.de) ist Leiter des Bereichs Improvement & Innovation bei QAware und Dozent an der Fachhochschule Rosenheim. Sein Hauptinteresse gilt der Softwarequalitätssicherung, der technologieübergreifenden Softwarearchitektur und Softwareproduktionsumgebungen.
Links & Literatur
- [1] GNU Trove: trove4j.sourceforge.net
- [2] Commons Primitives: jakarta.apache.org/commons/primitives/
- [3] fastutil: fastutil.dsi.unimi.it
- [4] FindBugs, Bug Pattern Detector: findbugs.sourceforge.net
- [5] Johannes Nowak: Fortgeschrittene Programmierung mit Java 5, dpunkt Verlag, 2004
- [6] Angelika Langer, Klaus Kreft: Spracherweiterung: Die Sprachmittel und eine Implementierung von Java Generics, in Java Magazin 4.2004
- [7] Java 5.0 - Die Zähmung des Tigers: www.QAware.de/Downloads.php
- [8] Java Language Specification, Third Edition: java.sun.com/docs/books/jls/
- [9] FH Rosenheim, Seminar J2SE 5.0: www.fh-rosenheim.de/~siedersleben/J2SE5.htm















