Neuerungen in Java 5.0 Stefan Bitzer ([email protected]) Sandra Lorenz ([email protected]) Alexander Weisser ([email protected]) Berufsakademie Stuttgart Außenstelle Horb Florianstraße 15 72160 Horb a.N. Studium IT Seminararbeit Software Engineering Betreuer: Prof. Dr. Olaf Herden November 2005 Abstract: Sun hat sich mit der Entwicklung von Java 5.0 (auch Java 1.5.0) das Ziel gesetzt, die Entwicklung mit Java zu vereinfachen und dabei die Abwärtskompatibilität zu gewährleisten. 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.0 erkennt und in herkömmlichen Java-Code übersetzt. Die vorliegende Dokumentation beschäftigt sich mit den Spracherweiterungen, die Java 5.0, auch Tiger-Release genannt, mit sich gebracht hat. Seit Ende September 2004 steht die endgültige Version zum Download bereit. Es werden dabei die neuen Sprachkonstrukte vorgestellt und mit Beispielen verdeutlicht. Aber auch die Vor- und Nachteile der neuen Möglichkeiten werden aufgezeigt. Für das Verständnis dieser Ausarbeitung sind einigermaßen gute Kenntnisse von Java bis Version 1.4 nötig. -2- Inhaltsverzeichnis 1 2 Generische Programmierung..................................................................................... 4 1.1 Generische Datentypen....................................................................................... 5 1.2 Klassenschablonen.............................................................................................. 8 1.3 Methodenschablonen ........................................................................................ 10 1.4 Typeeinschränkung........................................................................................... 11 1.5 Wildcards.......................................................................................................... 13 1.6 Änderungen an der Collection API................................................................... 16 1.7 Mögliche Fehlerquellen beim Umgang mit Generics ....................................... 18 Sonstige Erneuerungen ........................................................................................... 19 2.1 Vereinfachte Syntax für die Iteration................................................................ 19 2.2 Autoboxing / Autounboxing ............................................................................. 21 2.3 Syntaktische Unterstützung für Aufzählungswerte (Enumerations)................. 22 2.4 Statischer Import............................................................................................... 24 2.5 Erweitertes Look and Feel ................................................................................ 26 2.6 Annotations – Metadaten im Quellcode ........................................................... 27 2.7 Variable Parameterübergabe............................................................................. 29 3 Zusammenfassung................................................................................................... 30 4 Literaturverzeichnis ................................................................................................ 31 -3- 1 Generische Programmierung Eine der wichtigsten Neuerungen in Java 5.0 ist die Einführung der so genannten Generics. Sie bringen eine deutliche Veränderung mit sich, die sich vor allem in einem kleineren Codieraufwand niederschlagen, was später in diesem Abschnitt aufgezeigt wird. Aus Sicht der Programmierer war dieser Schritt schon lange überfällig. Da Java jedoch ursprünglich den Anspruch hatte, möglichst unkomplizierte Programmierung zu ermöglichen, wurde bei der Konzeption von Java dieser Aspekt bewusst ausgelassen. In der Sprache C++ ist eine Programmierung mit generischen Datentypen schon von Anfang an implementiert und unter den so genannten Templates bekannt. Die Templates stellen jedoch eine komplett andere Realisierung dar und können eigentlich nicht direkt mit den Generics in Java verglichen werden. Gleichzeitig werden die Generics in den Klassen der Collection API konsequent umgesetzt und sollten deshalb in neuen Programmen auch eingesetzt werden. Collection API ist der Name für die bei Java mitgelieferten Klassen, in denen viele Funktionen schon zur Verfügung stehen und nicht extra programmiert werden müssen. Durch den Einsatz der in Java 5.0 neuen Möglichkeiten, die unter dem Namen Generics zusammengefasst sind, ist eine Typ-Sicherheit im Umgang mit Objekten fast garantiert, denn eventuell falsche Typzuweisungen werden schon zur Compile-Zeit erkannt. Ganz ohne Hindernisse können die Generics jedoch auch nicht eingesetzt werden, denn es gibt immer wieder Stolperfallen, die beachtet werden müssen. Diese Einführung zeigt solche Problemstellen auf. Der vom neuen Compiler erstellte Bytecode ist abwärtskompatibel, denn er erzeugt den gleichen Code wie Java 1.4. Dies wird im Prinzip so realisiert, dass die neuen Sprachkonstrukte von einer Art Präprozessor erkannt und dann die entsprechenden Konstrukte erstellt werden. Dabei ist zu beachten, dass ein Java-Programm durch diese neuen Konstrukte keinesfalls schneller wird, sondern nur übersichtlicher im Quellcode erscheint. Im Gegenteil: Durch die einfachen Konstrukte wird der Programmierer sogar dazu verleitet, schlechten Code zu schreiben. Es ist nicht mehr sofort erkennbar, dass an den jeweiligen Stellen teilweise unnötige und zeitaufwendige Typecasts zur Laufzeit durchgeführt werden müssen! Die allgemeine Syntax der Generics erfordert an verschiedenen Stellen nach dem Klassennamen die Nachstellung des durch spitze Winkelklammern eingeschlossenen Typnamen. So wird eine Liste, die ausschließlich Objekte des Typs Integer enthält als List<Integer> = new List<Integer>(); definiert. Wie in Java durch die Objektorientierung üblich, sind diese neuen Festlegungen nur mit Objekten möglich. Primitivtypen können nicht verwendet werden. -4- Im Folgenden werden nun die Vorteile der neuen Sprachkonstrukte aufgezeigt. Aber auch entstandene Nachteile und Probleme bei der Verwendung bzw. in der Laufzeitoptimierung sollen nicht unaufgezeigt bleiben. Es wird dabei auf die an verschiedenen Stellen hinzugefügten Möglichkeiten eingegangen. Dies betrifft den Bereich generische Datentypen, die Möglichkeit Klassen- bzw. Methodenschablonen anzulegen genauso, wie das Verwenden von Typeinschränkungen und Wildcards und die Änderungen an der Collection API. 1.1 Generische Datentypen Warum sollen nun generische Datentypen eingesetzt werden? Bisher ging es doch auch ohne! Nun, in Java kann man schon von Anfang an durch die gegebene Vererbungshierarchie sämtliche allgemein benötigten Funktionen für die Klasse Objekt programmieren, von der alle (nicht-primitiven) Datentypen erben. Dadurch muss eine Funktion wie zum Beispiel das Sortieren oder auch Collections wie Listen nur einmal programmiert werden und können für jeden Typ von Daten verwendet werden. Diese Eigenschaft wird auch Polymorphie genannt. Doch genau hier treten einige Probleme auf. Die Konzeption von Java sieht im Prinzip vor, dass sämtliche Typen zur Compile-Zeit überprüft werden. Ein Aufruf Auto meinAuto = new Auto(); Haus meinHaus = meinAuto; wobei Auto nicht von Haus erbt, gibt einen Fehler in der 2. Zeile beim Compilieren, der zum Abbruch führt. Dies ist auch vollkommen richtig so. Die Überprüfung wurde jedoch bisher nicht überall konsequent in Java umgesetzt. Es können beispielsweise beliebige Typen zur Laufzeit in die Collections eingefügt werden. Es ist also ohne weiteres bis Version 1.4 erlaubt, zunächst String-Objekte, dann IntegerObjekte, dann irgendwelche eigenen Objekte und so weiter in eine Liste einzufügen. Da es alles Objekte sind, die eingefügt werden, meldet hier weder der Compiler einen Fehler, noch gibt es zur Laufzeit des Programms Probleme. Wenn die Objekte jedoch später im Programmablauf wieder aus der Liste geholt werden, müssen sie auf den entsprechend gewünschten Typ mit einem Cast zurückgeführt werden. Wird hier nun ein anderes Objekt zurückgegeben als erwartet, gibt es zur Laufzeit einen Fehler, der vom Compiler nicht erkannt wurde: Stack meinStack = new Stack(); meinStack.push("hallo"); meinStack.push(Integer(2)); //korrekt //auch korrekt String rueckgabe = (String) meinStack.pop(); -5- //Fehler!! An diesem Punkt setzen die generischen Datentypen an. Das Ziel ist, dass solche Fehler bereits beim Compilieren erkannt werden und so der Testaufwand der Programme deutlich verkleinert werden kann. Es müssen dann nicht mehr sämtliche möglichen Programmpfade aufgefunden und durchlaufen werden. Dies wird so realisiert, dass beispielsweise dem verwendeten Collection-Objekt der Typ mit angegeben wird, der darin gespeichert werden soll. Dabei werden die schon erwähnten spitzen Winkelklammern verwendet: List meineListe<String> = new List<String>(); Mit dieser Angabe überprüft der Compiler beim Übersetzen des Programms bei jeder Einfügeoperation, ob ein gültiges Objekt übergeben worden ist. Wenn nicht, bricht er mit einem Fehler ab. Gleichzeitig spart sich der Programmierer beim Herausnehmen eines Objektes den expliziten Typecast zu programmieren – er wird automatisch gemacht. So ist zu jeder Zeit eine korrekte Behandlung der Objekte ohne unschöne Typecast-Errors zur Laufzeit garantiert: List meineListe<String> = new List<String>(); meineListe.add("hallo"); meineListe.add(Integer(2)); //korrekt //Fehler beim Compilieren Mit dieser Compiler-Erweiterung wurden natürlich auch neue Fehlermeldungen nötig. So gibt der Compiler nun beim Auffinden eines dem Beispiel entsprechenden Fehlers einen Fehler über die inkompatiblen Typen im Quelltext aus. Um mit vorherigen Versionen von Java abwärtskompatibel zu bleiben, sprich dass alter Quellcode weiterhin compiliert werden kann, ist die Angabe des Typs in den spitzen Winkelklammern nicht zwingend notwendig. Werden sie weggelassen unterlässt der Compiler die Type-Überprüfung, gibt aber eine entsprechende Warnung aus, die den Programmierer darauf hinweisen, dass hier prinzipiell Probleme auftreten können. Dies funktioniert in beiden Richtungen. Es kann also eine herkömmlich programmierte Klasse ohne Probleme in einem Programm mit Generics instanziiert werden und auch umgekehrt. Folgender Code ist also gültig: -6- public class Test { public static void funktion1(List a) { … } public static void funktion2(List<Integer> a) { … } public static void main(String[] args) { List<Integer> auflistung = new ArrayList<Integer>(); funktion1(auflistung); ArrayList auflistung2 = new ArrayList(); funktion2(auflistung2); } } Es wird aber empfohlen, bei Gelegenheit alle Klassen auf Generics umzustellen, da alle vom Compiler ausgegebenen Warnungen eine potentielle Risikostelle darstellen und deshalb als richtige Fehler verstanden werden sollten. An diesen Stellen können weiterhin Typcast-Errors zur Laufzeit entstehen! Durch die Abwärtskompatibilität entstehen aber auch wieder Probleme, die bei der Programmierung beachtet werden müssen und sonst wieder zu den alten Problemen führen. Folgender Code wird mit Generics für stack1 korrekt kompiliert, verursacht aber einen Laufzeitfehler, da stack2 beliebige Objekte erlaubt: Stack<Integer> stack1 = new Stack<Integer>(); Stack stack2 = stack1; // stack2 referenziert auf stack1 stack2.push(Color.RED); // erlaubt - stack2 ist allgemein Integer neu = stack1.pop(); // Fehler! Es ist also weiterhin wichtig, dass der Programmierer beim Erstellen des Quellcodes sorgfältig vorgeht, denn sonst entstehen schnell wieder die alten Probleme. Diese können nur dadurch entstehen, dass es auf der Bytecode-Ebene keine Generics gibt. Sie werden nur zur Compile-Zeit beachtet und dann entfernt. Aus diesem Grund kann der Typ zur Laufzeit auch nicht auf den Generics-Typ abgefragt werden. Der Aufruf System.out.println(stack1.getClass() == stack2.getClass()); -7- liefert nicht wie vielleicht erwartet false, sondern true. Der Grund ist, dass beide Objekte ganz normale Instanzen von Stack sind, die im Prinzip mit Objekten arbeiten. Nur der Compiler kennt den Unterschied und macht die Typüberprüfungen. So kann auch nicht mit stack1 instance of Stack<Integer> //geht nicht überprüft werden, ob es sich um einen Stack handelt, der nur Integer-Werte akzeptiert. 1.2 Klassenschablonen Wie schon eingangs erwähnt, sind die Generics in Java an die Templates in C++ angelehnt, jedoch an einigen Stellen deutlich verbessert worden. Das Ziel, das hinter dieser Idee steckt, ist das Einsparen von Programmieraufwand und somit die Steigerung der Übersichtlichkeit durch weniger Quellcodezeilen. Es soll ermöglicht werden, dass eine Klasse bzw. Methode nur einmalig für viele verschiedene Objekt-Typen geschrieben werden muss. In Java ist dies schon seit Anfang an möglich. Realisiert wurde es dadurch, dass sämtliche Objekt-Typen von Object erben. Eine Methode, die nun allgemein verwendet werden soll, muss nun nur den Typ Object erwarten und ermöglicht so die Übergabe verschiedenster Typen. Dies bedeutet aber auch, dass nicht sinnvolle Parameter übergeben werden können. Hier setzen die Generics an. Eine Klasse kann so definiert werden, dass dem davon instanziierten Objekt über die Generics-Notation der explizite Typ, der zum Einsatz kommen soll, übergeben wird: -8- public class Test<T> { private T Wert; public Test(T Wert) {} public setValue(T Wert) { this.Wert = Wert; } public T getValue() { return Wert; } } public class Haupt { public static void main(String[] args) { Test versuch<String> = new Test<String>(); versuch.setValue("hallo"); System.out.println(versuch.getValue); } } Die in den spitzen Winkelklammern übergebene Variable kann einen beliebigen Namen (außer den reservierten Symbolen) haben, sollte aber aus Gründen der Übersicht nur aus einzelnen Grossbuchstaben bestehen. Der übergebenen Variable wird beim Erstellen eines Objektes von der Klasse der zu verwendende Typ mit angegeben. Er kann dann innerhalb der Klasse überall verwendet werden. Es kann nun notwendig sein, Klassen mit bestimmten Abhängigkeiten von unterschiedlichen Typen zu erstellen. Um hier die Typsicherheit innerhalb der Klasse zu garantieren, ist es sinnvoll der Klasse mehr als nur einen generischen Typ zu übergeben: public class Test<T, V> { private T Wert1; private V Wert2; … } Im Gegensatz zu C++ wird jedoch nicht für jeden im Programm implementierten Typ eigener Code erzeugt, sondern immer der gleiche verwendet und nur der Typ zur Compile-Zeit geprüft. Dadurch wird gegenüber C++, was dort auch als Code Bloat bezeichnet wird, unnötiger Code gespart. Durch dieses in Java 5.0 neue Vorgehen ist die Typsicherheit trotz der hohen Flexibilität in der Verwendung der Klasse gewährleistet. -9- 1.3 Methodenschablonen Wie bei den parametrisierten Klassen können auch Methoden mit den neuen Möglichkeiten der Generics erstellt werden: static <T> int suche(T[] quelle, T suchobjekt) { for(int i = 0; i < quelle.length; i++) if(suchobjekt.equals(quelle[i])) return i; } return -1; } Dabei muss beim Aufruf der generischen Methode der gewünschte Typ nicht mit übergeben werden. Er wird automatisch erkannt. Die Funktion kann also einfach folgendermaßen aufgerufen werden: Array meineListe<String> = new Array<String>(); // Array hier auffüllen if(suche(meineListe, "hallo") != -1) System.out.println("gefunden!"); Der Compiler überprüft die übergebenen Parameter. So sind Aufrufe wie z.B. System.out.println(suche(meineListe, Integer(3))); ungültig und erzeugen einen Fehler schon beim Compilieren. - 10 - 1.4 Typeinschränkung Durch die Schablonen ist es nun möglich, Methoden bzw. Klassen für unterschiedliche Typen zu verwenden um Code einzusparen. Bei bestimmten Anwendungen aber führt dies zu einem Laufzeitfehler, wenn die übergebenen Objekte nicht von dem erwünschten Typ sind, beziehungsweise dieses Objekt nicht die geforderte Funktionalität beinhaltet. So ist es zum Beispiel möglich, dass es trotz der Verwendung von Generics zu Fehlern kommen kann, wenn Objekte bestimmte Eigenschaften nicht besitzen, die aber innerhalb einer Klasse oder Methode verwendet werden. Um den Typ der Objekte, die übergeben werden, von vorne herein bereits einzuschränken und somit Fehler zu vermeiden, kann mit den Generics eine Typeinschränkung vorgenommen werden. Diese Typeinschränkung erfolgt mit dem in Java eingesetzten Schlüsselwort extends. So ist es zum Beispiel möglich, nur Objekte innerhalb einer Methode zu erlauben, die vom Type String bzw. Stringbuffer sind: public static <T extends Charsequence> T (T val1, T val2) { … } In dieser Methode sind der Rückgabewert sowie die beiden Übergabewerte (im Typ) durch das Schlüsselwort extends Charsequence eingeschränkt. Es kann aber auch sehr gut möglich sein, das nicht alle Objekte diese Einschränkung bzw. unterschiedliche Einschränkungen erhalten sollen. Um das zu ermöglichen, müssen mehrere generische Typen übergeben werden, wobei jedes Objekt eine eigene Einschränkung erhalten kann: public static <T extends Charsequence, V extends Comparable> T (T val1, V val2) { … } Das Schlüsselwort extends zwingt somit dazu, dass nur Objekte verwendet werden können, die bestimmte Schnittstellen bzw. Eigenschaften besitzen. Dies bringt den Vorteil, dass bereits zur Compile-Zeit überprüft werden kann, ob alle Objekte, die diese Methode verwenden, auch von ihren Eigenschaften her dazu berechtigt sind. Viel interessanter ist es aber noch, dass auch Schnittstellen mit Generics erstellt werden können und es erlaubt ist, diese zur Typeinschränkung zu verwenden. Die Definition beim Interface sieht dann so aus: interface IStack<T> { public void push(T object); public T pop(); } - 11 - Um das Interface einsetzen zu können, müssen bei der Definition der Klasse ebenfalls Generics verwendet werden, um den Typ an das Interface zu übergeben. public class Stack<T> implements IStack<T> { public IntegerStack() { top = new StackStorage(); } public void push(Integer obj) { StackStorage store = new StackStorage(); store.prev = top; top = store; store.object = obj; } public Integer pop() { Integer ret = top.object; top = top.prev; return ret; } } Hier müssen nun jeweils die Methoden push und pop des Interfaces implementiert werden. Aber genauso wie es bei Methoden möglich ist, die Typen über das Schlüsselwort extends einzuschränken, ist es auch möglich dies bei den Interfaces zu verwenden. Der Aufruf sieht folgendermaßen aus: public class Stack<T> implements IStack<U extends T> { … } - 12 - 1.5 Wildcards Bisher waren ausschließlich Instanziierungen von parametrisierten Typen möglich, bei denen der Typparameter durch einen konkreten Typ wie Integer ersetzt waren. Das heißt, das wenn man eine Liste einsetzt, die anhand Generics erzeugt wurde, es nicht möglich ist diese Liste mit artverwandten Objekten des instanziierten Typs zu füllen, was auch gezielt von den Javaentwicklern so gewollt ist. Ein Beispiel ist List<Integer>: LinkedList<Integer> list = new LinkedList<Integer>(); list.add(new Integer(0)); Integer i = list.get(0); Hier ist es nicht möglich, die Liste mit anderen Objekten als Interger Objekten zu füllen. Artverwandte Objekte währen hier Objekte wie Long, Double, also kurz gesagt, alle abgeleiteten Objekte vom Typ Number. Durch diese Einschränkung war zwar der Typ gesichert, aber es gibt auch Situationen, in denen nicht nur ein Typ zulässig ist, sondern auch Typfamilien beziehungsweise Objekte, die sich in bestimmten Eigenschaften nicht unterscheiden. Um dies mit den Generics verwirklichen zu können, müssen nicht konkrete Typen für den Typparameter eingesetzt werden, sondern so genannte Wildcards. Es gibt 3 Arten von Wildcards: <? extends [Type]> <? super [Type]> <?> Wildcard-Instanziierungen bestehen aus einem Fragezeichen, das den unbekannten Typ darstellen soll und so gesehen als Platzhalter für diesen unbekannten Typ fungiert. Diese Wildcard-Instanziierungen gibt es ungebunden sowie gebunden. Sind die generischen Typen gebunden, ist nur eine bestimmte Menge an Objekten zulässig. Sie sehen dann zum Beispiel folgendermaßen aus: List<? extends Number> List<? super Long> List<?> Ein solches Wildcard bezeichnet keinen konkreten Typ, sondern eine Familie von Typen. Das Wildcard <? extends Number> bezeichnet beispielsweise die Menge aller Typen, die vom Typ Number direkt oder indirekt abgeleitet sind, d.h. die Familie aller Subtypen von Number, also Long, Integer, usw., inklusive Number selbst und zählt zu der Familie der gebunden Wildcards. Die Wildcard-Instanziierung List<? extends Number> - 13 - bezeichnet daher logischerweise die Familie aller Instanziierungen des parametrisierten Typs List, bei dem ein Typ aus der Familie der Subtypen von Number für den Typparameter eingesetzt wurde, also List<Long>, List<Integer>, usw. inklusive List<Number>. Ein Wildcard wie: <? super Long> bezeichnet die Familie aller Supertypen von Long ohne Long an sich selbst und somit auch ein gebundenes Wildcard, beziehungsweise auch parametrisiertes Wildcard genannt. Das Wildcard <?> steht für die Menge aller Typen ohne irgendwelche Einschränkungen. Dieses Wildcard wird auch „unbounded wildcard“ genannt, was somit wie in der Version von Java 1.4 der Verwendung ohne Generics entspricht. Durch die explizite Angabe jedoch erkennt der Compiler, dass dieses Vorgehen (keine genaue Angabe des Typs) gewünscht ist und gibt keine Warnung aus. Eine Wildcard-Instanziierung eines parametrisierten Typs kann nicht dazu verwendet werden, ein Objekt zu erzeugen. Es ist zwar möglich ein Objekt vom Typ List<? extends Number> zu deklarieren, aber es ist nicht möglich ein Objekt vom Typ List<? extends Number> zu initialisieren. Ein nicht initialisiertes Objekt vom Typ List<? extends Number> kann aber auf Objekte von kompatiblen Typen verweisen. Schachtel<?> kiste; //ist gültig kiste = new Schachtel<Apfel>(); //Objekt wird initialisiert Schachtel<Apfel> apfelKiste = new Schachtel<?>(); //ist nicht gültig Der letzte Aufruf ist nicht gültig, da der Compiler keinen freien Willen hat, deswegen kann er keine „beliebige“ Schachtel erzeugen. Die kompatiblen Typen sind genau die Typen aus der Familie von Typen, die die Wildcard-Instanziierung mit extends oder super bezeichnet. Ein Referenzobjekt vom Typ List<? extends Number> kann also auf ein Objekt vom Typ List<Long> oder List<Integer> usw. verweisen. Analog kann ein Referenzobjekt vom Typ List<? super Long> auf Objekte vom Typ List<Long>, List<Number> oder List<Comparable> usw. verweisen. Ein Objekt vom Typ List<?> kann auf beliebige Instanziierungen von List verweisen. Um die Verwendungsmöglichkeiten der Wildcards zusammen mit den Einschränkungen etwas ansehnlicher darzustellen, ist ein kleines Beispiel aus dem Alltag notwendig. Gehen wir davon aus, dass es eine Klasse Fahrzeuge gibt. Eine abgeleitete Klasse ist dann die Klasse Auto. Als Liste definieren wir eine Garage, in der Fahrzeuge parken können. - 14 - Setzen wir nun die Generics ein, so kann eine Garage die von der Klasse Fahrzeuge instanziiert wurde alle Objekte vom Typ Fahrzeug aufnehmen. Das heißt, dass Fahrzeuge vom Typ Fahrzeug in dieser Garage parken können. public void parken(<Fahrzeug> was) { garage.add(was); // Code zum Einfügen eines Fahrzeugs(wo) in Fahrzeugliste … } Nun vermutet man, dass es möglich ist, in dieser Garage ein Auto zu parken, da es ja ein Fahrzeug ist. Genau dies aber lässt Java nicht zu, um das Konzept der Generics nicht zu unterwandern. Um nun aber in dieser Garage auch Autos bzw. andere Fahrzeuge, die von der Klasse Fahrzeug abgeleitet sind, zu parken, müssen die Wildcards verwendet werden. Dies funktioniert mit dem Schlüsselwort extends. Die Wildcards erlauben es, beliebige Objekte aufzunehmen. Wie beliebig, hängt von der Einschränkung ab. public void parken(<? extends Fahrzeug> was) { garage.add(was); // Code zum Einfügen eines Fahrzeugs(wo) in Fahrzeugliste … } Nun darf die Garage auch Autos und andere Fahrzeuge aufnehmen, die nicht direkt vom Typ Fahrzeug sind. Parametrisierte Methode oder Einsatz von Wildcards? Es können verschiedene Möglichkeiten zur Definition einer Methode mit generischen Datentypen bestehen. Es kann eine parametrisierte Methode erstellt werden oder Wildcards eingesetzt werden: static <T extends Comparable> List<T> sortList(List<T> list); static List<? extends Comparable> sortList(List<? extends Comparable> list); Welche Möglichkeit sollte aber nun verwendet werden? Normalerweise sollte die Möglichkeit mit den Wildcards eingesetzt werden. Nur wenn der Typ auch innerhalb der Methode bzw. auch als Rückgabetyp benötigt wird, sollte die parametrisierte Methode verwendet werden. - 15 - 1.6 Änderungen an der Collection API Durch die Neuerungen in Java in Bezug auf die Generics musste die Collection API auch auf die neuen Möglichkeiten angepasst werden. Alle Funktionen in der Collection API unterstützen nun die Generics und erlauben dem Programmierer somit, einen bedeutend sichereren Code als früher zu schreiben. Dies war nicht immer ganz einfach, da die Abwärtskompatibilität vollkommen gewährleistet bleiben sollte. Dabei mussten einige spezielle Dinge beachtet werden. Ein Beispiel ist die Methode Collections.max, die das größte Element in einer Collection zurückgibt. Die traditionelle Signatur dieser Methode ist wie folgt definiert: public class Collections { public static Object max(Collection coll); } Um diese Methode max nun mit den generischen Typen und vollständig kompatibel zur ursprünglichen Methode zu machen, ist folgende nun folgende Definition in Java 5.0 aktuell: public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll) Mit dem &-Zeichen getrennt, kann man mehrere Bounds für den Parametertyp angeben, der jeweils erste wird immer für die tatsächliche Implementierung, also für die interne Generierung des gelöschten Typs, verwendet. In die neue Collection API fanden aber auch einige neue Pakete, Klassen und Schnittstellen Einzug. Im Folgenden eine kleine Auflistung. Nähere Informationen können der Dokumentation zu Java (Javadoc) entnommen werden. Die wichtigsten hinzugekommene Pakete sind: • java.lang.annotation (Gibt Bibliothekunterstützung für den Java NotationsService) • java.lang.instrument (Stellt Dienste für die JVM zur Verfügung) • java.lang.management (Stellt die Managementschnittstelle für die Überwachung und für das Management der JVM sowie dem Betriebssystem zur Verfügung, auf dem die JVM läuft) • java.util.concurrent • javax.sql.rowset (Standard-Interfaces und Klassen für JDBC RowSet) • javax.xml.xpath (beinhaltet ein Objektmodell zur Auswertung von XPathAusdrücken) - 16 - Wichtige hinzugekommene Klassen und Schnittstellen • Closeable (Interface) • Appendable (Interface) • Iterable (Interface) • ProcessBuilder • Type, TypeVariable (Interface) • StringBuilder (die nicht-synchronisierte Variante von StringBuffer) • Formatter (Java 2 Platform SE v1.5.0) • Queue (Interface) • UUID (universeller einmaliger Identifier) • MouseInfo • PointerInfo - 17 - 1.7 Mögliche Fehlerquellen beim Umgang mit Generics Da die Einführung der Generics in Java nur darauf beruht, dem Compiler verschiedene Anweisungen zur Behandlung verschiedener Objekte zu überwachen und prüfen, gibt es auch einige Sprachkonstrukte, die nicht erlaubt sind, aber nicht als solche sofort erkennbar sind. Generische Typen sind in statischem Kontext nicht erlaubt: class Test<T> { static T meineKonstante; // Fehler static T meineRueckgabe() { … } // Fehler static void meineMethode(List<T> liste) { … } // Fehler } Da die generischen Typen nur auf ihre schon aus älteren Java-Programmen bekannten Objekten zurückgeführt werden, machen solche Aufrufe schlichtweg keinen Sinn. Genauso dürfen Klassen bzw. Methoden mit generischen Typparametern nicht überladen werden: class Versuch<T, U> { void setze(T erster) { … } void setze(U zweiter) { … } // Fehler } - 18 - 2 Sonstige Erneuerungen In diesem Kapitel wird auf die Spracherweiterungen von Java 5.0 eingegangen, die neben der generischen Programmierung eine wichtige Bedeutung spielen. 2.1 Vereinfachte Syntax für die Iteration Die for-Schleife ist erheblich erweitert worden. Es ist möglich, mit einer einfacheren Syntax durch verschiedene Strukturen zu iterieren. Dies gilt unter anderem für Arrays und Collections1. Ein Beispiel für das Iterieren durch Arrays Auf die herkömmliche Weise: public static void printArray1(int[] args) { for (int i = 0; i < args.length; ++i) { System.out.println(args[i]); } } Erweiterte for-Schleife: public static void printArray1(int[] args) { for (int i : args) { System.out.println(i); } } Das Erzeugen und Inkrementieren der Hilfsvariablen i, das Herausfinden des ArrayEnde und der explizite Zugriff auf die Array-Variable kann mit Hilfe der erweiterten for-Schleife eingespart werden. Der Code wird einfacher und klarer. 1 Eine Collection bezeichnet Datenstrukturen wie Listen, Stacks, Schlangen, Bäume und Mengen im mathematischen Sinne - 19 - Ein Beispiel für das Iterieren durch Collections: Auf die herkömmliche Weise: public static void printVector1(Vector v) { for (Iterator it = v.iterator(); it.hasNext(); ) { Object o = it.next(); System.out.println(o); } } Erweiterte for-Schleife: public static void printVector1(Vector v) { for (Object 0 : v) { System.out.println(o); } } In der neuen Variante kann mit Hilfe der erweiterten for-Schleife auf die explizite Deklaration und Verwendung des Iterators2 verzichtet werden. Beide Varianten haben das gleiche Ergebnis, nur ist der zweite Code einfacher zu lesen und intuitiv zu verstehen. Der Doppelpunkt ist als „in“ zu lesen. Wie bei der normalen for-Schleife kann man in der Schleife Variablen deklarieren, wie hier in der Variante 1. Greift man auf die neue for-Schleife zurück und kombiniert das Ganze mit der Verwendung von Generics, kann eine Menge Code eingespart werden. Die so „optimierten“ for-Schleifen sind auch besser lesbar. Der Compiler übernimmt hierbei die gesamte Arbeit, um die neue Schleifenart zu implementieren. Dabei wird der Code wieder in die Darstellung der herkömmlichen Weise umgewandelt. Die neue for-Schleife schützt auch vor Fehlern, die leicht bei der herkömmlichen forSchleife auftreten können. Zum Beispiel verursacht for(int i = 0; i < x.length; i++) { for(int j = 0; j < x.length; j++) { System.out.println(x[i] * y[j]); } } 2 Ein Iterator ist ein Interface zum Durchlaufen der Elemente einer Collection - 20 - einen Fehler bei der Index-Obergrenze des Feldes y, da die Schleifenvariable j, die auch als Index für y verwendet wird, tatsächlich über die Länge des Arrays x läuft. Solche Fehler werden mit Hilfe der neuen for-Schleife verhindert. 2.2 Autoboxing / Autounboxing Ein Nachteil der Collections war bisher, dass sie nur Elemente von komplexen Datentypen aufnehmen konnten. Wollte man Elemente einfacher Datentypen wie int, long, char etc. einfügen, war bisher eine passende Wrapper-Instanz notwendig. Beim Entnehmen mussten die Elemente manuell „entpackt“ werden. Ab Java 5.0 übernimmt der Compiler die Aufgabe, primitive Datentypen in ihre Wrapper und umgekehrt zu konvertieren. Die Vorgehensweise wird auch Autoboxing genannt, da der Compiler einen Grunddatentyp automatisch in einen Objekttyp umwandelt, so als ob er diesen in eine spezielle Schachtel verpacken würde. Der umgekehrte Weg wird mit Unboxing bezeichnet. Dadurch dass der Compiler die notwendigen Konvertierungen übernimmt, werden Fehler vermieden, die erst zur Laufzeit auftreten würden, wenn ein Objekt auf einen falschen Datentyp gecastet wird. Beispiel von Boxing und Autoboxing: int i = 111; Integer j = i; int k = j; //Boxing: j = Integer.valueOf(i) //Unboxing: k = j.intValue() Ein weiteres Beispiel: Auf die herkömmliche Weise: list.add(0, new Integer(59)); int n = ((Integer)(list.get(0))).intValue(); Autoboxing / Autounboxing: Durch das Auto(un)boxing fällt diese manuelle Konvertierung weg. Das oben angegebene Codebeispiel kann somit folgendermaßen vereinfacht werden: list.add(0, 59); int total = list.get(0); - 21 - 2.3 Syntaktische Unterstützung für Aufzählungswerte (Enumerations) Bisher wurden Aufzählungen über verschiedene Implementierungen auf Klassenebene gelöst. Allerdings gab es dabei oft Einbußen in der Performance und es muss häufig derselbe Code geschrieben werden. Java 5 ermöglicht richtige Aufzählungen, mit Hilfe des Schlüsselwortes enum, welches früher schon reserviert, aber nicht belegt war. Enumeration ist ein Datentyp mit einem endlichen Wertebereich. Alle Werte des Aufzählungstyps werden bei der Deklaration des Typs als Name definiert. Man unterscheidet typenlose Aufzählungen, die lediglich Namen für numerische Werte festlegen, und typsichere Aufzählungen. Typsichere Aufzählungen verhindern, dass „Äpfel mit Birnen“ verglichen werden. Typesafe Enumerations (enum) besitzen im einfachsten Fall einen Namen und bestehen aus einer Element-Menge dieses Typs wodurch Typsicherheit erreicht wird. Die einzelnen Werte der Konstanten des enum lassen sich mittels der Methode valueOf ermitteln. Benötigt man eine Aufzählung aller Werte des enum, so bietet sich die Klassenmethode values an. Enums sind fast vollwertige Klassen, so lassen sich neben den oben aufgeführten Methoden weitere Objekt- und Klassenmethoden sowie Objektund Klassenattribute frei definieren. Die Haupteinschränkung gegenüber Klassen besteht darin, dass man weder auf Merkmale der Oberklassen zugreifen kann, noch dass man eine Klassenhierarchie mittels enums aufbauen darf. Ein enum darf keine Unterklasse besitzen. Besondere Bedeutung finden enums in switch/case-Anweisungen, denn dort können hinter dem Schlüsselwort case nur Werte verwendet werden, die sich implizit in intWerte wandeln lassen. Da diese wenig aussagekräftig sind, verwendeten Entwickler oft int-Werte, die als statische Konstanten unter sprechenden Namen in einer Klasse definiert sind. Die so definierten int-Enumerations haben einige Nachteile: • Sie bieten keine Typsicherheit, da die Verwendung der Konstanten nicht überprüft wird • Sie bilden keinen „richtigen“ Namensraum • Sie können nicht mit Collection-Instanzen verwendet werden • Bei Ausgabe werden die int-Werte und nicht die eigentlich gewünschten Konstantennamen ausgegeben Diese Nachteile lassen sich durch den Einsatz des Typsafe enum Entwurfsmusters umgehen. Enum-Konstanten sind in switch-Anweisungen möglich, da sie intern über eine Ganzzahl als Identifikator verfügen, der vom Compiler eingesetzt wird. - 22 - Vorteile von enum: • enums bieten Typsicherheit zur Übersetzungszeit • enum-Konstanten können gelöscht, hinzugefügt oder umsortiert werden, ohne dass die sie verwendenden Klassen neu übersetzt werden müssen. • enums lassen sich in switch/case-Anweisungen verwenden. Beispiel public enum Farbe { rot, gelb, weiss, violett }; Da es sich bei einer typesafe Enumeration im Grunde um eine Klasse handelt, können innerhalb einer typesafe Enumeration zusätzlich Attribute, Methoden und parametrisierte Konstruktoren definiert werden. Damit ist es möglich den Elementen weitere Eigenschaften zuzuweisen. Typesafe Enumeration mit Konstruktor, Methoden und Attributen: public enum Blume { Rose("Topf"), Akelei("Garten"), Mohn("Feld"); Blume(String standort) { this.standort = standort; } private final String standort; public String getStandort() { return standort; } }; Bei der Implementierung von typesafe Enumerations gibt es aber auch ein paar Regeln zu beachten: • Ein Defaultkonstruktor darf nicht implementiert werden, da dieser implizit bereitgestellt wird • Die Typesafe Enumeration besitzt automatisch die beiden Methoden values() und valueof(String) sowie die Methoden der Klasse java.lang.enum • Die Klasse enum ist implizit immer statisch • Deklarierte Attribute und Methoden sind automatisch statisch - 23 - 2.4 Statischer Import Fast jede Java-Anwendung verwendet Konstanten. Diese sind oft zentral in einer Klasse statisch definiert. Das hat den Nachteil, dass bei der Nutzung der Klassenname vorangestellt werden muss. Bisher konnte dieses Problem mit Hilfe eines Interfaces gelöst werden, welches die Konstanten definiert, ansonsten aber nichts deklariert. Die Klassen die diese Konstanten nutzen wollen, mussten das Interface implementieren. Dadurch können die Konstanten ohne vorangestellten Klassennamen genutzt werden. Diese Lösung funktioniert, ist aber aus sprachtechnischer Sicht zu verwerfen, denn Interfaces dienen der Deklaration neuer Typen und nicht als Konstanten-Container. Die import static Deklaration ist der normalen import-Anweisung sehr ähnlich. Während import die Einbindung von Klassen ohne die Qualifizierung des Paketnamens ermöglicht, erleichtert import static die Einbindung von statischen Mitgliedern einer Klasse ohne die Qualifizierung des Klassennamens. Beispiel auf die bisherige Weise: package com.name; interface XYZ { public static final double Constant1 = someValue; public static final double Constant2 = anotherValue; } //Verwendung der Konstanten des XYZ-Interface public class MyClass implements XYZ { … double value = 2* Constant1; … } Beispiel mit der Spracherweiterung import static: import static com.name.XYZ.*; public class MyClass { … double value = 2 * Constant1; … } Ein weiteres Beispiel: import static java.lang.Math.*; import static java.lang.System.*; - 24 - Mit Hilfe dieser zwei statischen Imports kann man sin(x) anstatt Math.sin(x) und Welt") anstatt System.out.println("Hallo Welt") verwenden. out.println("Hallo Fehlerquellen bei der Verwendung von import static: Die Überlagerung von statischen Importen mit Methoden oder Feldern der Klasse kann zu Fehlern führen. Hierfür ist festgelegt, dass ein statischer Import nie eine interne Definition der Klasse überschreibt. Der Compiler meldet aber keinen Fehler. Dadurch kann es besonders bei Wartungsarbeiten an einer Klasse unklar sein, welche Methode gerade benutzt wird und falsche Annahmen getroffen werden. Der statische Import sollte sparsam verwendet werden, zum Beispiel in Fällen, in denen man geneigt wäre lokale Kopien von Konstanten anzulegen oder Konstanten in Schnittstellen zu deklarieren, um diese später durch Vererbung einzubinden. Was auch bedacht werden sollte ist, dass ein übermäßiger Gebrauch von statischen Importen zu unlesbaren Programmen führen kann. Das ist aber genau das Gegenteil wofür die statischen Importe eingeführt wurden. - 25 - 2.5 Erweitertes Look and Feel Das Standard-Java-Look&Feel wird in der J2SE 5.0 beispielsweise durch das Ocean L&F abgelöst, was keine völlig neue, aber dennoch angepasste Darstellung der grafischen Elemente zeigt. Zusätzlich gibt es noch das anpassbare Synth L&F. Der Vorteil gegenüber anderen L&F ist bei Synth, dass es nicht über Programmierung sondern durch eine XML-Datei konfigurierbar ist und leicht durch zusätzliche Skins erweiterbar ist. Die Klasse JTable wurde mit einer Druckunterstützung ausgerüstet und kleinere Änderungen von Komponenten können direkt über die Methode add() in ein JFrame eingefügt werden. Beispiel: JFrame jf = new JFrame(); JButton jb = new JButton(); jf.add(jb); Java 2D wurde um die Hardware-Unterstützung von OpenGL erweitert. Das Image I/O API unterstützt zusätzlich lesend und schreibend die Grafikformate BMP und WBMP. Das AWT enthält neue Methoden zur Ermittlung der aktuellen Mausposition, zum dauerhaften Setzen von Fenstern in den Vordergrund und dem Setzen der Standardposition eines neuen Fensters. Für Java unter Unix wurde die Oberflächenbibliothek AWT für das X Window System überarbeitet. Das neue XAWT malt seine Bildschirmelemente selbst und ist nicht mehr von den Motif- und Xt-Bibliotheken abhängig. Unter Linux soll das neues XAWT GUIAnwendungen um den Faktor 6,5 beschleunigen. - 26 - 2.6 Annotations – Metadaten im Quellcode Annotations ermöglichen es, Java-Code mit zusätzlichen Informationen anzureichern, die z.B. für andere Anwendungen nützlich sind. Die Annotations ermöglichen dem Entwickler eigene Tags zu definieren, mit denen eigene Klassen, Attribute und Methoden für ein spezielles Werkzeug gekennzeichnet werden können. AnnotationsTags beginnen immer mit dem @-Zeichen. Durch die Verwendung von Annotations lassen sich zur Compile-Zeit und zur Laufzeit über die class-Dateien automatisiert Informationen auslesen. Der Vorteil von Annotationen gegenüber anderen Möglichkeiten der Bereitstellung von Informationen liegt in der fest definierten Funktionsweise und den Zugriffsmöglichkeiten über ein API. Über Annotationen erhält man einen Mechanismus um Informationen zur automatisierten Auswertung weiterzugeben. Es gibt bereits Metadaten in Java. Ein Beispiel dafür ist JavaDoc. Mit JavaDocKommentaren kann beispielsweise die Versionsnummer und der Name des Autors einer Klasse angegeben werden. Beispiel zu JavaDoc: /** @version 1.0 @author Mustermann */ public class JavaDoc { … } Anstatt nur auf vordefinierte Felder wie Autor oder Version zurückgreifen zu können, kann der Benutzer seit Java 5.0 die Felder selbst definieren. Die Struktur der Metadaten wird wie ein Java Interface beschrieben, das den Typ der Annotation festlegt. Dem interface-Schlüsselwort wird ein @ vorangestellt, um Metadaten von herkömmlichen Schnittstellen zu unterscheiden. Soll die Annotation Parameter besitzen, muss für jeden Parameter eine Methode mit dem entsprechenden Rückgabewert erzeugt werden. Der Name der Methode entspricht dabei dem Namen des Parameters. Bei der Deklaration von Metadaten sind folgende Einschränkungen zu beachten: • Es ist nicht erlaubt, Metadatenstrukturen zu vererben. Jede Struktur erbt implizit von der Schnittstelle java.lang.annotation.Annotation. • Die Methoden dürfen keine Parameter haben • Die Methoden dürfen als Rückgabetyp lediglich primitive Typen wie int oder float, String, Class, Enumeration, Annotationstypen und Arrays der vorigen Typen verwenden. Außerdem sind auch generische Methoden verboten. • Methoden können keine Exceptions werfen. Insbesondere ist also die Verwendung von throws verboten. - 27 - • Annotationstypen können nicht parametrisiert werden. Mit Hilfe der Reflection API kann man Annotations auch zur Laufzeit einer Anwendung auswerten. Anwendungsbeispiele: • Annotations können Informationen für andere Anwendungen bereitstellen. Die externen Anwendungen generieren aufgrund der Informationen der Annotations Interfaces und/oder Klassen • Jeder Entwickler kann über Annotations für eine Methode, Klasse usw. Informationen hinterlegen. Diese können automatisiert gesammelt und ausgewertet werden. • Erzeugen automatischer Todo-Listen Beispiel: import java.lang.reflect.*; import java.lang.annotation.*; //Unterstütz. für die Annotat. public class TodoAnnotationTest { @Todo(beschreibung={"Beispiel mit Annotations", klasse = TodoAnnotationTest.class, wertigkeit=3, bem=@Bemerkung("keine")) public static void main(String args[]) { Methode[] methoden = TodoAnnotationTest.class.getMethods(); for(Method m : methoden) { if(m.isAnnotationPresent(Todo.class)) { System.out.println("Methoden " + m.getName()); Todo td = m.getAnnotation(Todo.class); System.out.println(td.beschreibung()[0]); System.out.println(td.wertigkeit()); System.out.println(td.bem().value()); } } } } Annotationen werden in Zukunft eine große Rolle bei der dynamischen Generierung von Java-Quellcode spielen. Durch die einfache Beschreibung der benötigten Informationen über Annotationen wird sich der Aufwand auf Entwicklerseite verringern. Außerdem wird der Java-Quellcode weniger fehleranfällig und besser lesbar sein. Allerdings ist zu beachten, dass weder der Java-Quelltext noch die class-Dateien kompatibel mit älteren Java-Versionen sind. - 28 - 2.7 Variable Parameterübergabe Unter variabler Parameterübergabe versteht man, dass beim Aufruf einer Methode eine beliebige Anzahl und beliebige Typen von Parametern übergeben werden können. Die Idee bei variabler Parameterübergabe besteht darin, eine einzige Methode zu implementieren, die eine beliebige Anzahl von Datentypen von Argumenten entgegennehmen kann. Ein Paradebeispiel für variable Parameterübergabe ist die Methode printf aus der Programmiersprache C. Die Zeichenkette „…“ dient dem Übersetzer als Schlüsselwort, dass eine variable Anzahl von Parametern folgt. Beim Aufruf werden nun alle folgenden Parameter in ein Array gepackt und der Methode als Parameter übergeben. In der Implementierung der Methode wird dieses Array Element für Element wieder ausgelesen. Die variable Parameterübergabe ist eine reine Compilerleistung, die dem Programmierer das Leben sehr vereinfacht. Beispiel: class Computer { public static int sum (int … values) {…} } Aufruf dieser Methode: int x = Computer.sum(1,2); int y = Computer.sum(1,2,3); Was passiert, wenn man neben der Methode mit variabler Anzahl von Objekten noch die folgenden Methoden implementiert? public static void print(Object … args); public static void print(Object arg); public static void print(Integer … args); Da alle Methoden unterschiedliche Signaturen besitzen, lässt sich der Programmtext problemlos übersetzen. Aufrufbeispiele und die zur Laufzeit aufgerufene Methode: print(5, 7.0, "35,4"); print("35,4"); print(5, 7, 15); print(5); //print(Object … args) //print(Object arg) //print(Integer … args) //print(Object arg) Es wird immer die Methode mit der am besten passenden Signatur aufgerufen, wobei die Anzahl der Parameter eine höhere Priorität besitzt als der Datentyp des Parameters. - 29 - 3 Zusammenfassung Java 5.0 bringt nicht nur neue Klassenbibliotheken und ein verbessertes Laufzeitverhalten mit sich, sondern erweitert auch die Sprachsyntax umfangreich. Hier zahlt sich die Flexibilität des Java-Interpreters und der Laufzeitumgebung aus. Sie gestattet es auch komplexe sprachliche Neuerungen aufzunehmen ohne einen Umbau in der Basis zu fordern. Die neuen Sprachmittel bieten viele Möglichkeiten, die die JavaProgrammierung ausdrucksstärker und einfacher machen. Mit den Generics erübrigt sich die lästige Typumwandlung von Objekten, die zusammen mit abstrakten Datentypen verwendet werden. Dies macht nicht nur den Code einfacher zu lesen, sondern erspart dem Entwickler auch einige Laufzeitprobleme, die nun schon vom Compiler entdeckt werden können. Allerdings ist Vorsicht im Umgang mit den Generics geboten, denn es können schnell Laufzeitprobleme entstehen, wenn häufige Casts ausgeführt werden, die im Quellcode nicht mehr direkt ersichtlich sind. Die neue for-Schleife erspart Tipparbeit und schützt vor Fehlern. Autoboxing und Unboxing ersparen einem zwar nicht Laufzeitfehler, aber es erleichtert die Programmierarbeit, da die manuelle Konvertierung zwischen primitiven Datentypen und ihren Wrapper-Klassen wegfällt. Wie auch Generics geben Enumerationen dem Entwickler mehr Typsicherheit. Durch die variable Parameterübergabe ist es nicht mehr nötig mit Listen oder Arrays von Parametern zu arbeiten. Statische Imports ersparen dem Entwickler ebenso wie die forSchleife und das Autoboxing Tipparbeit. Der fest in der Java-Syntax verankerte Annotationstyp erlaubt dem Entwickler beliebige Metadaten zu definieren, ohne sich auf die Verwaltungsinfrastruktur eines Drittanbieters stützen zu müssen. Zusammenfassend lässt sich sagen, dass die Erweiterungen Java zu einer wesentlich mächtigeren, einfacheren und vor allem robusteren Sprache machen. Trotzdem muss noch viel Arbeit geleistet werden, um die neue Komplexität zum Beispiel durch die Generics und die Fehlerpotenziale wie zum Beispiel beim Autoboxing und statischer Importe in den Griff zu bekommen. - 30 - 4 Literaturverzeichnis [AL05] Angelika Langer: Generics, Auflage vom 2. September 2005 http://www.angelikalanger.com/GenericsFAQ/FAQSections/Parameterized Types.html#WIldcard%20Instantiations, aufgerufen im Oktober 2005. [BF05] Böttcher, Ulrike; Frischalowski Dirk: Java 5 - Programmierhandbuch, 2005. [BS04] Bernhard Steppan: Schneller programmieren mit Java 5.0, Computerwoche 43/2004, Seite 32. [BU04] Burd Barry: Java2 für Dummies. Mitp-Verlag/Bonn, 4. Auflage 2004. [CEH+05] Chandrasekhara Vasus, Dr. Andreas Eberhart, Dr. Horst Hellbrück, Stefan Kraus, Dr. Ulrich Walther: Java 5.0 Konzepte, Grundlagen und Erweiterungen in 5.0, Carl Hanser Verlag München Wien, 2005. [CU04] Christian Ullenboom: Erste Neuerungen in Java 1.5. [JA05] Josef Adersberger: Mit dem Tiger in der Falle, Java Magazin Ausgabe 8.2005. [JH03] Jason Hunter: Big Changes Coming for Java, Oracel Magazine September/October 2003. [NW05] Maurice Naftalin, Philip Wadler: Generics and Collections in Java 5, O’Reilly Media, Inc. 2005. [QHM04] Qusay H. Mahmoud: The All-New Java 2 Platform, Standard Edition (J2SE) 5.0, http://java.sun.com/, aufgerufen im September 2005 [UB03] Dr. Ulrich Breymann: Tiger auf dem Sprung, c’t 2003, Heft 26, Heise Zeitschriften Verlag GmbH & Co.KG. [VS05] Venkat Subramaniam: Generics in Java – Part I http://www.agiledeveloper.com/, aufgerufen im Oktober 2005. - 31 - bis III,