Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Java 5: Taming the Tiger Syntactic Sugar JSR-201 Seminararbeit Enterprise Computing SS 05 Autor: Renato Melliger Fachhochschule Aargau Departement Technik Studiengang Informatik Betreuender Dozent: Prof. Dr. Dominik Gruntz Windisch, 8. April 2005 I Abstract This work deals with the JSR 201, the new syntactic features of the Java 2 Platform, Standard Edition version 5.0 (Java 5), called “Syntactic Sugar”. These new possibilities make the writing and reading of code easier and reduce the tipping of program code. The first improvement is a new for loop called For-Each Loop that simplifies the iteration over collection-like objects. The conversion from primitive data types to their wrapper classes and back has been automated by Autoboxing which means that for example an int value can be allocated an Integer and vice versa. Static fields and methods of a class can be added to a namespace using the extended static import statement. The typesafe enum allows an uncomplicated definition and use of enumerations by using the keyword enum. And the last of the new syntactic features are variable arguments that give the possibility of one method definition for the using of many methods with different number of arguments. This feature enables the use of a printf method comparable to the printf known from C/C++. [ JSR 201, For-Each Loop, Autoboxing, Static Import, Typesafe Enums, Varargs ] II Inhaltsverzeichnis 1 EINLEITUNG 1 2 FOR-EACH LOOP 2 2.1 2.2 2.3 2.4 3 ITERIEREN ÜBER COLLECTIONS DIE NEUE SYNTAX SEMANTIK UND PERFORMANCE ANWENDUNG AUTOBOXING 3.1 3.2 3.3 3.4 4 STATIC IMPORT 6.1 6.2 6.3 6.4 6.5 7 4 5 5 6 7 ZUGRIFF AUF STATISCHE MEMBER EINER KLASSE DIE NEUE SYNTAX ANWENDUNG TYPESAFE ENUMS 5.1 5.1.1 5.1.2 5.2 5.3 5.4 5.5 5.6 6 4 ELEMENTARE DATENTYPEN UND DEREN WRAPPERKLASSEN DIE NEUE SYNTAX HINTERGRÜNDE ANWENDUNG 4.1 4.2 4.3 5 2 2 3 4 7 7 8 8 ENUM PATTERNS DAS INT ENUM PATTERN DAS TYPESAFE ENUM PATTERN DIE NEUE SYNTAX VORTEILE ENUMSET, ENUMMAP HINTERGRÜNDE ANWENDUNG 8 8 9 10 11 12 12 13 VARARGS 14 METHODEN MIT UNBEKANNTER ANZAHL PARAMETER DIE NEUE SYNTAX HINTERGRÜNDE DIE METHODE PRINTF ANWENDUNG SCHLUSSWORT 14 14 15 16 17 17 III Syntactic Sugar 1 Einleitung Dieser Seminar-Bericht befasst sich mit der neuesten Version 1.5.0 von Java - auch bekannt unter Java 5.0 - mit dem Namen „Tiger“. Obwohl beide Versionsnummern den neuesten Release von Java beschreiben, bezeichnet 5.0 die Produktnummer des Java 2 Platform Standard Edition 5.0 Development Kit (JDK 5.0), während 1.5 der Stand der Entwicklung von Java bezeichnet. In diesem Bericht wird nicht unterschieden zwischen Produkt- und Entwickler-Version. Die Version 5 ist ein grosser Meilenstein in der Entwicklung von Java. Sie umfasst neben neuen Klassenbibliotheken und Erweiterungen der alten Klassenbibliotheken interessante neue Spracheigenschaften. Die wichtigste Neuerung sind die Generics, welche unter anderem den Vorteil von typsicheren Containern mit sich bringen. Eine andere wichtige Neuerung sind die Annotations. Dazu kommt das concurrent-Package, welches das Entwickeln von Multithread-Anwendung erleichtern soll. Zuletzt wurden auch einige syntaktische Spracherweiterungen gemacht, welche sich ausschliesslich auf die SprachSyntax auswirken. Um genau diesen „syntaktischen Zucker“ geht es in diesem Bericht. Diese Erweiterungen, welche bereits aus C# bekannt sind, sollen das Programmieren wie auch das Lesen von Programmcode erleichtern und dem Programmierer unnötige Schreibarbeit abnehmen. Die Schreibarbeit wird neu vom Compiler durch das rewriten des Codes übernommen. Er übersetzt die Syntax so, dass sie von der Java Virtual Machine verstanden wird. Deshalb beschränken sich die Erweiterungen fast ausschliesslich auf Änderungen am Compiler, wogegen an der Java Virtual Machine keine Änderungen vorgenommen wurden. Dieser „Syntactic Sugar“ setzt sich aus fünf Neuerungen zusammen: Eine neue for-Schleife mit der Bezeichnung For-Each-Loop wurde eingeführt. Sie erleichtert das Iterieren über Collections und Arrays. Das Autoboxing automatisiert das Überführen eines elementaren Datentyps in seine Wrapperklasse und zurück. Statische Imports ermöglichen es, statische Felder und Methoden zu importieren, um diese ohne das vorangestellte Schreiben ihrer Klasse verwenden zu können. Typesafe Enums ermöglichen es, typsichere Enumerationen unkompliziert zu definieren und zu nutzen. Diese vier Neuerungen wurden mit dem JSR 201 eingeführt. Die letzte Neuerung sind die Varargs aus dem JSR 176. Sie bieten die Möglichkeit, Methoden mit variablen Argumenten zu definieren. Jede Neuerung wird in einem eigenen Kapitel vorgestellt. Dabei wird das Problem, aus dem die Neuerung entstanden ist, geschildert, die neue Syntax aufgezeigt, Beispiele, was bei der Verwendung beachtet werden muss, angegeben und Besprochen. Der Bericht richtet sich an Programmierer, die bereits ein breites Wissen im Bereich von Java 1.4.2 oder auch früheren Versionen mitbringen und an den syntaktischen Neuerungen von Java 5 interessiert sind. 1 - 19 Syntactic Sugar 2 For-Each Loop 2.1 Iterieren über Collections Das Iterieren über eine Collection stellte bereits in früheren Java-Versionen kein grosses Problem dar. Jedoch war es immer mit dem Instanzieren eines Iterators und somit unnötiger Tipparbeit verbunden. Der Standard-Code, um in früheren Java-Versionen über eine Collection c, welche ausschliesslich Strings enthält, zu iterieren, sieht wie folgt aus: for( Iterator i = c.iterator(); i.hasNext(); ) System.out.println( (String) i.next() ); Mit den in Java 1.5 eingeführten Generics folgendermassen geschrieben werden: kann diese for-Schleife for ( Iterator<String> i = c.iterator(); i.hasNext(); System.out.println( i.next() ); vereinfacht ) Es muss hier kein String-Cast mehr vorgenommen werden, da die Generics die Möglichkeit bieten, typensichere Collections und Iteratoren zu definieren. Jedoch wird auch hier ein Iterator definiert, der nur in der Schleife verwendet wird. Der Iterator wird am Anfang der Schleife ein Mal deklariert und initialisiert. Bei jedem Schleifendurchlauf werden dann zwei Methoden des Iterators aufgerufen. Danach wird der Iterator nicht mehr verwendet. Dieses Deklarieren, das Definieren und Aufrufen des Iterators bringen unnötigen Tippaufwand mit sich. In Java 1.5 wurde nun eine vereinfachte Syntax eingeführt. Sie verbessert die Leserlichkeit, verkleinert den Tippaufwand und vermeidet das Deklarieren eines temporären Iterators. Diese Syntax wird als For-Each Loop oder auch als Enhanced For Loop bezeichnet und kann für „Behälter“ (Objekte, über welche iteriert werden kann) und Arrays verwendet werden. Sie ist bereits aus C# bekannt, was wohl auch der Grund für ihre Einführung war. 2.2 Die neue Syntax Die Syntax für die erweiterte for-Schleife kann wie folgt dargestellt werden: for ( [Typ] [temporäre Variable] : [Iterable/Array] ) [Statement] Das in der Einleitung angedeutete Beispiel wird mit dieser neuen Syntax folgerndermassen geschrieben: 2 - 19 Syntactic Sugar for ( String s : c ) System.out.println( s ); Es wird über die String-Collection c iteriert, indem in jedem Schleifendurchlauf der Variable s das nächste Element (in diesem Beispiel ein String) der Collection zugewiesen wird. Diese Variable s, also das aktuelle Element der Collection, kann so im Rumpf der Schleife angesprochen werden. Das Objekt c muss das Interface java.lang.Iterable implementieren. Es bestimmt, ob für einen „Behälter“ die For-Each-Schleife verwendet werden kann oder nicht. Dieses Interface ist ebenfalls erst seit der Java-Version 1.5 enthalte. Verschiedene „Behälter-Interfaces“ wie java.util.List, java.util.Set, java.util.Collection, usw. werden vom Interface java.lang.Iterable abgeleitet. Es enthält auschliesslich die Methode iterator(), welche einen Iterator zurückliefert. Der Doppelpunkt ( : ) wird dabei als „in“ gelesen. Das obige Beispiel liest sich demnach wie folgt: „Für jedes String-Element s in der Collection c …“. Wie bereits erwähnt, ist diese Syntax nicht nur für Objekte, die das Interface java.lang.Iterable implementieren einsetzbar, sondern auch für Iterationen über Arrays. Jedoch ist hier zu beachten, dass nur Iterationen über das gesamte Array möglich sind, da die Elemente nicht mehr über ihre Indizes angesprochen werden. Die folgende Methode verwendet die For-Each-Schleife um die Werte eines int-Arrays zu summieren: int sum( int[] a ) { int result = 0; for( int i : a ) result += i; return result; } 2.3 Semantik und Performance Beim Betrachten einer For-Each-Schleife ist kein Iterator zu sehen, trotzdem instanziert der Compiler im Hintergrund einen Iterator. Die For-Each-Schleife bringt also nur Verbesserung bezüglich der Syntax, wogegen keine Änderungen an der Semantik gemacht werden. Deshalb verhält sie sich genau gleich wie eine For-Schleife mit einem Iterator, wirft demnach auch dieselben Exceptions. Deshalb bringt die For-Each-Schleife bezüglich der Performance keine Verbesserung. Interessant wird die For-Each-Schleife, wenn Änderungen am Behälter gemacht werden, während darüber iteriert wird. Das folgende Programm zeigt das Verhalten: public static void main(String[] args) { int[] a = new int[]{1,2,3,4,5}; int size = a.length; for(int i : a){ System.out.println(i); a[size-1-i] = 0; } } 3 - 19 Syntactic Sugar Das Programm generiert folgenden Output: 1 2 0 0 0 Der Grund für diesen Output ist, dass in der Schleife über eine Referenzkopie iteriert wird, was bedeutet, dass sich Änderungen auf der Referenzkopie auf die ursprünglichen Felder auswirken und diese dann gegebenenfalls ändern. 2.4 Anwendung Die For-Each-Schleife sollte immer dann angewandt werden, wenn dies semantisch möglich ist. Da sie den Iterator verbirgt, ist es nicht möglich, einige Methoden, welche einen Iterator erfordern aufzurufen. Zum Beispiel können keine Elemente mit der Methode remove() aus einer Collection entfernt werden, da die Methode remove() einen Iterator benötigt. 3 Autoboxing 3.1 Elementare Datentypen und deren Wrapperklassen Obwohl Java als typisierte objektorientierte Programmiersprache bezeichnet wird, ist im Gegensatz zu durchgängig objektorientierten Programmiersprachen wie zum Beispiel Smalltalk, in Java nicht alles ein Objekt. Es wird unterschieden zwischen elementaren („primitiven“) Datentypen und Referenztypen, hinter denen sich die Objekte verbergen. Durch diesen Umstand ist es oftmals nötig, einen elementaren Datentypen in seine Wrapperklasse zu überführen und umgekehrt. Diese Vorgänge werden als „Boxing“ und „Unboxing“ bezeichnet, was soviel bedeutet wie Ein- und Auspacken der elementaren Datentypen in ihre Wrapper-Objekte. Sollen beispielsweise alle Elemente eines int-Arrays in einer Collection gespeichert und dann die Summe dieser Collection berechnet werden, muss genau das gemacht werden. Der Programm-Code mit Verwendung von Generics und der For-Each-Schleife sieht wie folgt aus: int[] array = { 1, 2, 3, 4 }; Collection<Integer> collection = new ArrayList<Integer>(); for( int wert : array ) collection.add( Integer.valueOf(wert) ); int summe = 0; for( Integer wert : collection ) 4 - 19 // Boxing Syntactic Sugar summe += wert.intValue(); // Unboxing System.out.println(“Die Summe zählt: “ + summe); In der ersten For-Each-Schleife werden die Werte des Arrays in die Collection gespeichert. Da Collections nur Objektreferenzen und keine elementare Datentypen speichern können, muss der int-Wert des Arrays in ein Integer gepackt werden (Boxing). Dies kann entweder mit dem Konstruktor erfolgen oder wie im Beispiel mit der statischen Methode valueOf. In der zweiten Schleife werden die Werte der Collection aufsummiert. Um die Werte zu Addieren, müssen die int-Werte aus den Integer-Objekten entpackt werden (Unboxing). Dieses Boxing und Unboxing wird nun in Java 1.5 vom Compiler übernommen und deshalb als Autoboxing und Autounboxing bezeichnet. 3.2 Die neue Syntax Das oben gezeigte Beispiel, welches die Elemente eines int-Arrays in einer Collection speichert und dann die Summe der Collection berechnet, sieht mit der neuen Syntax wie folgt aus: int[] array = { 1, 2, 3, 4 }; Collection<Integer> collection = new ArrayList<Integer>(); for( int wert : array ) collection.add( wert ); // Autoboxing int summe = 0; for( Integer wert : collection ) summe += wert; // Autounboxing System.out.println(“Die Summe zählt: “ + summe); Das Umwandeln vom elementaren Datentypen in dessen Wrapper-Objekt und das Umwandeln des Wrapper-Objekts in dessen elementaren Datentypen wird hier vom Compiler übernommen und muss vom Programmierer nicht mehr beachtet werden. Der Compiler ruft für das Boxing die statische Methode valueOf auf, für das Unboxing wird die Methode intValue aufgerufen. Das automatische Boxing und Unboxing gilt für die Datentypen Byte (umgewandelt in byte und umgekehrt), Short (in short), Character (in char), Integer (in int), Long (in long), Float (in float), Double (in double) und Boolean (in boolean). 3.3 Hintergründe Die Regel besagt also, dass alle elementaren Datentypen und dessen Wrapperklassen das Autoboxing und Autounboxing unterstützen. Für das Unboxing gilt zusätzlich die Regel: Falls kein Objekt vorhanden ist (null-Referenz), so wird eine NullPointerException geworfen. Der folgende Code zeigt dies: Integer i1 = null; 5 - 19 Syntactic Sugar int i2 = i1; // Exception Da das Objekt i1 in einen elementaren Datentypen gewandelt werden soll, jedoch keinen Wert enthält, sondern auf null verweist, erzeugt die zweite Zeile hier eine NullPointerException. Es wird kein Default-Wert für i2 gesetzt, was in dieser Situation auch nicht der Logik entsprechen würde. Um zwei Wrapper-Instanzen von elementaren Datentypen zu vergleichen, wird wie bis anhin die Methode equals() aufgerufen. Sie vergleicht die Werte, die in der Wrapperklasse gespeichert sind. Werden aber die zwei Wrapperklassen mit dem Operator == verglichen, so werden normalerweise die Referenzen, also die Adressen der Objekte verglichen. Werden aber zwei Integer- oder Long-Objekte verglichen, welche den gleichen Wert zwischen -128 und 127 gespeichert haben, so wird als Resultat true zurückgegeben, sonst false. Der folgende Code zeigt dieses Verhalten: for( int i = -130; i < 130; i++ ) { Integer i1 = i; Integer i2 = i; System.out.println(i1 + “ = “ + (i1==i2)); } Das Programm gibt für alle Werte von -128 bis 127 true aus, während für die anderen false ausgegeben wird. Die Erklärung dafür ist ein Cache, in dem für alle Werte zwischen -128 und 127 vorgefertigte Integer- oder Long-Objekte eingetragen sind. Wird dann für einen bestimmten Wert in diesem Bereich ein Wrapper-Objekt angefordert, liefert der Cache stets dasselbe bereits vorgefertigte Objekt für diesen Wert zurück. Da immer dasselbe Objekt zurückgegeben wird, sind dessen Referenzen auch gleich und ergeben im Vergleich mit == true zurück. Für die anderen Werte, die nicht im erwähnten Bereich liegen, wir jedes Mal ein neues Objekt zurückgegeben, wodurch beim Vergleich mit == false zurückgegeben wird, da die Referenzen nicht gleich sind. Ob diese Implementierung wirklich sinnvoll ist, sollte jedoch bezweifelt werden. Ein zweites spezielles Verhalten ist bei der Zuweisung eines int-Wertes an ein Long-Objekt zu beobachten: Long l = 5; // Compilerfehler Hier meldet der Compiler einen Fehler, weil zuerst die Typenkonvertierung gemacht wird und dann festgestellt wird, dass die beiden Typen inkompatibel sind. Es wird demnach zuerst der int-Wert in dessen Wrapperklasse verpackt und dann bemerkt, dass Integer und Long nicht kompatibel sind. Es wird nicht versucht, zuerst den elementaren Datentypen zu erweitern, also den int-Wert in einen long-Wert zu überführen, und erst dann die Konvertierung vorzunehmen. Dann würde der Compiler keinen Fehler melden. Es ist deshalb nötig, der Variablen des Typs Long einen long-Wert zuzuweisen: Long l = 5L; 3.4 Anwendung Autoboxing erleichtert den Umgang mit elementaren Datentypen im Kontext der Referenztypen, behebt aber nicht den Umstand, dass elementare Typen keine Objekte 6 - 19 Syntactic Sugar sind. Die Trennung dieser Typen bleibt also erhalten, einzig wird das Umwandeln vom Compiler automatisch ausgeführt. Deshalb sollte das Autoboxing nur dort verwendet werden, wo es auch wirklich gebraucht wird. Denn da int und Integer nicht gleichzusetzen sind und der Compiler den einen in den anderen Typen umwandeln muss, wird so Rechenzeit in Anspruch genommen. Werden grosse Mengen an Daten so umgewandelt, wird die Performance eines Programms dadurch stark beschränkt. 4 Static Import 4.1 Zugriff auf statische Member einer Klasse Um auf statische Attribute und Methoden einer Klasse zuzugreifen, mussten diese bislang zusätzlich mit dem Namen ihrer Klasse angesprochen werden. Der Java-Code um eine mathematische Funktion ausführen zu können, welche den Sinus von PI berechnet sah dann folgendermassen aus: double wert = Math.sin( Math.PI ); Die Klasse java.lang.Math, in der die statische Methode sin und das Attribut PI definiert sind, müssen jeweils mit dem Namen der sie definierenden Klassen qualifiziert werden. Neu ist es nun möglich, auf das Schreiben der Klasse vor dem Attribut oder der Methode zu verzichten, indem ein statischer Import definiert wird. 4.2 Die neue Syntax Um das unnötige Schreiben einer Klasse zu umgehen, wurden in Java 1.5 die statischen Importe eingeführt. Bislang war es möglich, Klassen und Interfaces zu importieren, um das aufwändige Schreiben des Package-Namens zu umgehen. Nun ist es auch möglich, statische Attribute und Methoden einer Klasse ohne den Klassennamen zu schreiben. Folgender Code zeigt das obige Beispiel, wie es in Java 1.5 geschrieben werden kann: double wert = sin ( PI ); Damit der Compiler die statische Methode sin und das statische Attribut PI findet, müssen diese statisch importiert werden. Dies wird mit den folgenden zwei Importanweisungen erreicht: import static java.lang.Math.sin; import static java.lang.Math.PI; 7 - 19 Syntactic Sugar Um alle Felder der Klasse java.lang.Math zu importieren, kann das folgende Statement verwendet warden: import static java.lang.Math.*; Die Imports von sin und PI sind dann überflüssig. Dieses Verfahren sollte bereits aus den Imports von Klassen und Interfaces bekannt sein. 4.3 Anwendung Statische Importe sollten nur spärlich verwendet werden. Erst dann, wenn ein statisches Element mehrere Male verwendet wird, sollte ein statischer Import in Erwägung gezogen werden. Ein Übertriebenes Verwenden kann schnell zu unübersichtlichem und schwer lesbarem Programmcode führen. Meist ist dann nur mühsam herauszufinden, welcher Klasse ein Element angehört. Alle statischen Elemente einer Klasse ( .*-Notation ) sollten erst dann eingefügt werden, wenn mehr als drei bis vier verschiedene Elemente derselben Klasse verwendet werden. 5 Typesafe Enums 5.1 Enum Patterns In Java gab es bislang noch keine vom Compiler unterstützte Enumeration (Aufzählung), kurz Enums. Deshalb wurden dafür zwei Patterns verwendet, die diese Funktion bereitstellen. Jedoch kämpfen beide Patterns mit gewissen Problemen. 5.1.1 Das int Enum Pattern Das int Enum Pattern besteht aus statischen int-Konstanten, die entweder direkt dort wo sie verwendet auch definiert werden oder aber sie werden in einer eigens erstellten Klasse oder Interface definiert. Ein int Enum Pattern für die vier Jahreszeiten würde dann für die zweit genannte Variante wie folgt aussehen: class Season public public public public } { static static static static final final final final int int int int SPRING SUMMER AUTUMN WINTER = = = = 0; 1; 2; 3; 8 - 19 Syntactic Sugar Den vier Variablen SPRING, SUMMER, AUTUMN und WINTER wurden die int-Werte von 0 bis 3 zugewiesen. Eine Jahreszeit kann dann folgendermassen in der Variable currentSeason gespeichert werden: int currentSeason = Season.SPRING; Der Nachteil dieses Patterns ist, dass es nicht typensicher (typsafe) ist: Es ist möglich, der Variable currentSeason einen nicht definierten int-Wert, beispielsweise 4 zuzuweisen, oder zwei Konstanten der Klasse Season zusammenzuzählen. Beide Aktionen würden keinen Sinn ergeben. Um diese Probleme zu lösen, wurde das Typesafe Enum Pattern entwickelt. 5.1.2 Das Typesafe Enum Pattern Das typensichere Enum Pattern für die Jahreszeiten sieht wie folgt aus: class Season public public public public { static static static static final final final final Season Season Season Season SPRING SUMMER AUTUMN WINTER = = = = new new new new Season( Season( Season( Season( 0 1 2 3 ); ); ); ); public final int value; private Season( int value ) { this.value = value; } public String toString() { return “” + this.value; } } Eine Jahreszeit wird nun folgendermassen abgespeichert: Season currentSeason = Season.SPRING; Falls der Variablen currentSeason ein anderer Wert als eine der vier Jahreszeiten zugewiesen wird, meldet der Compiler einen Fehler. Ebenfalls ein Fehler wird angezeigt, wenn zwei Jahreszeiten zusammengezählt werden oder falls der Variablen ein Wert eines anderen Typs zugewiesen wird. Der Nachteil dieser Lösung ist, dass ein Enum-Wert nicht direkt in einer switch-Anweisung verwendet und nicht ohne weiteres über die Enum-Werte iteriert werden kann. Wie beim int Enum Pattern würde, falls die Variable currentSeason mit der Methode println ausgegeben werden sollte, nur ein int-Wert ausgegeben, anstatt dem Namen der Jahreszeit. Des Weiteren ist der Programmieraufwand für ein Typesafe Enum Pattern sehr hoch. Da beide Patterns gewisse Nachteile mit sich bringen wurde in Java 1.5 eine typensichere Enum-Klasse eingeführt. 9 - 19 Syntactic Sugar 5.2 Die neue Syntax Enumerationen sind die heimlichen Sieger von Java 1.5. Nach vielen Beteuerungen durch Sun, Enums seien in Java überflüssig und können einfach nachgebildet werden, wurden sie nun doch eingeführt. Die einfachste Möglichkeit einer Enumeration der Jahreszeiten sieht wie folgt aus: enum Season { SPRING, SUMMER, AUTUMN, WINTER; } Das Schlüsselwort enum steht für eine spezielle Art von Klasse, die eine Enumeration definiert. Die Jahreszeiten SPRING, SUMMER, AUTUMN und WINTER sind die Konstanten der Klasse. Im Gegensatz zu anderen Programmiersprachen wie C/C++ und C# kann man ihnen per Gleichheitszeichen keine ganzen Zahlen zuordnen. Sie werden automatisch von 0 her nummeriert. Im Beispiel erhält SPRING den Wert 0, SUMMER den Wert 1, AUTUMN den Wert 2 und WINTER den Wert 3. Die implizite Methode toString() gib nun nicht mehr den int-Wert der Konstante zurück, sondern dessen Namen. Die Deklaration der Enum-Klasse ist wie folgt festgelegt: public enum ClassName Interfaces opt { opt ConstName ( Argument, ... ) opt { AnonymousClassBody } ... ; ClassBody opt opt , } Dabei sind alle mit „opt“ bezeichneten Blöcke optional. Vor enum kann entweder nichts oder public stehen. Falls mindestens eine Methode im ClassBody abstrakt ist, ist die ganze Enum-Klasse abstract. Als Modifikatoren sind nur public oder default erlaubt. Eine Enum-Klasse kann wie jede andere Java-Klasse beliebige Interfaces implementieren. Im Rumpf der Enumeration werden zuerst die Konstanten aufgezählt. Sie sind voneinander durch Kommas getrennt, wobei der letzten ein Semikolon folgt. Hinter jeder Konstante können optional Argumente für Konstruktoren angegeben werden, sofern diese im ClassBody angelegt wurden. Damit keine Instanzen der Konstanten angelegt werden können, sind alle Konstruktoren implizit private. Zusätzlich kann ein Block AnonymousClassBody stehen, welcher Methoden implementiert, die für alle Konstanten aufgerufen werden können, aber für verschiedene Konstanten unterschiedlich implementiert sind. Damit diese Methoden von aussen aufgerufen werden können, muss die Methode auch im ClassBody definiert sein. Der Block ClassBody enthält gemeinsame Felder, Konstruktoren und Methoden der Enum-Klasse. Nur die Methoden können public deklariert werden, alles andere ist implizit private. Mit diesen Möglichkeiten könnte eine erweiterte Klasse für die Jahreszeiten folgendermassen aussehen: enum Season { SPRING( SUMMER( AUTUMN( WINTER( „fruehling“ ) , „sommer“ ), „herbst“ ), „winter“ ); 10 - 19 Syntactic Sugar private final String name; private Season ( String name ) { this.name = name; } public String toString() { return this.name; } } Die Enumeration besitzt nun zusätzlich einen allgemeinen Konstruktor, das Attribut name und überschreibt die Methoden getName, die nun das Attribut name ausgibt. Die Enumeration könnte nun wie folgt verwendet werden: Season currentSeason = Season.SPRING; Season nextSeason = Season.SUMMER; System.out.println( currentSeason); System.out.println( nextSeason.getName() ); Es werden zwei Konstanten der Enum-Klasse Season den jeweiligen Variablen zugewiesen. Die beiden Aufrufe der println-Methode haben den gleichen Zweck. Sie geben den Namen der jeweiligen Konstante aus. 5.3 Vorteile Die Enums beheben alle angesprochenen Nachteile, die beim Verwenden von EnumPatterns bestehen: Sie sind typensicher, die Methode toString() sorgt für eine informative Print-Ausgabe, eine direkte Verwendung in der switch-Anweisung ist möglich, eine problemlose Iteration über die Elemente ist möglich und enum-Konstanten können geändert, hinzugefügt oder gelöscht werden, ohne dass die Klasse, die sie verwendet, neu kompiliert werden muss. Folgendes Beispiel zeigt die Verwendung der oben definierten Jahreszeiten-Enum in einer switch-Anweisung: Season season = Season.SPRING; switch ( season ) { case SPRING: System.out.println( case SUMMER: System.out.println( case AUTUMN: System.out.println( case WINTER: System.out.println( default: System.out.println( } “spring” ); break; “summer” ); break; “autumn” ); break; “winter” ); break; “default” ); Die Klasse hat eine ähnlich hohe Performance wie das int Enum Pattern, verhindert Probleme mit den Namespaces und bietet die Möglichkeit, eigene Felder zu integrieren und Interfaces zu implementieren. Auch die Container EnumSet und EnumMap sind neu dazugekommen. 11 - 19 Syntactic Sugar 5.4 EnumSet, EnumMap EnumSet und EnumMap wurden für die Verwendung von Enumerationen in Collections entwickelt und sind hoch performante Implementierungen. Beide Collections sind nicht synchronisiert und deshalb nicht ohne weiteres threadsafe. EnumSet wie auch EnumMap können immer nur mit genau einer Enum verwendet werden. Das Verwenden von verschiedenen Enums in derselben EnumSet oder EnumMap ist nicht erlaubt. EnumSet ist abstrakt deklariert, was dazu führt, dass EnumSet-Objekte nur mit statischen Factories erzeugt werden können. Das folgende Beispiel zeigt die Definition einer EnumSet mit der Enum Season und wie sie verwendet werden kann: EnumSet<Season> es = EnumSet.allOf(Season.class); es.add(Season.SPRING); for(Season s: es) System.out.println(s); Anstatt der verwendeten Factory-Methode allOf können auch andere Factory-Methoden wie noneOf, of, copyOf, complementOf oder range verwendet werden. Ihre Bedeutungen wird in der Java 1.5 API von Sun genauer erläutert. Im Gegensatz zur Klasse EnumSet hat man sich bei der Klasse EnumMap für Konstruktoren entschieden. Das spezielle an der Klasse EnumMap ist, dass als Schlüssel eine EnumKonstante der in der Deklaration festgelegten Enum angegeben werden muss. Das folgende Beispiel zeigt die Definition einer EnumMap mit der Enum Season und wie sie verwendet werden kann: EnumMap<Season, String> em = new EnumMap<Season, String>(Season.class); em.put(Season.SUMMER, "Baden"); em.put(Season.WINTER, "Skifahren"); System.out.println(em.get(Season.WINTER)); System.out.println(em.get(Season.SUMMER)); 5.5 Hintergründe Obwohl die Java Virtual Machine für die Enumeration nicht geändert werden musste, waren neben dem Compiler auch Änderungen in den class-Dateien und dem SerialisierungsMechanismus von Java nötig. Der Compiler schreibt die einfachste Enumeration der Jahreszeiten enum Season { SPRING, SUMMER, AUTUMN, WINTER; } folgendermassen um: 12 - 19 Syntactic Sugar static class Season extends Enum { public static final Season SPRING; public static final Season SUMMER; public static final Season AUTUMN; public static final Season WINTER; private static final Season[] VALUES; static { SPRING SUMMER AUTUMN WINTER VALUES } = = = = = new new new new new Season( “SPRING”, 0 ); Season( “SUMMER”, 1 ); Season( “AUTUMN”, 2 ); Season( “WINTER”, 3 ); Season[] { SPRING, SUMMER, AUTUMN, WINTER }; private Season( String s, int i) { super( s,i ); } public static final Season[] values() ... public static final Season valueOf( String s ) ... } Dieser Code ist die Grundlage für die Erläuterung der Eigenschaften von Enumerationen: Alle enum-Klassen werden automatisch von der abstrakten Klasse Enum agbeleitet. Eine explizite Ableitung von Enum ist jedoch nicht möglich. Dadurch, dass der Konstruktor private deklariert ist, können keine Instanzen erzeugt werden. Die Konstanten sind self-typed, sie werden implizit als static final vom Typ ihrer enumKlasse (im Beispiel also Season) mit Hilfe eines static-Initializers angelegt. Dabei wird ein Konstruktor von Enum mit ihrem Namen als String und einem int-Wert, beginnend mit 0 aufgerufen. Die Methode valueOf(String s) liefert die Konstante zurück, welche mit dem Namen s verbunden ist und values() liefert ein Clone des internen Arrays aller Konstanten. Beide Methoden werden vom Compiler automatisch generiert. Als einzige Instanz-Methode kann toString() überschrieben werden. Alle anderen sind mit final deklariert. Die weiteren Klassen, welche von Enum geerbt werden, sind in der Java 1.5 API von Sun zu finden. 5.6 Anwendung Wird eine fixe Menge Konstanten benötigt, so sollen auf jeden Fall Enums verwendet werden. 13 - 19 Syntactic Sugar 6 Varargs 6.1 Methoden mit unbekannter Anzahl Parameter Um eine Methode bereitzustellen, welcher eine nicht festgelegte Anzahl Argumente übergeben werden kann, gab es bis anhin zwei Möglichkeiten. Einerseits konnte das Problem mit dem Mechanismus des Überladens von Methoden gelöst werden, was mit viel Programmieraufwand verbunden war. Der Code zeigt das Problem anhand einer Methode sum, welche die Summe der übergebenen int-Argumente berechnet: int sum( int return } int sum( int return } … p1, int p2 ) { p1+p2; p1, int p2, int p3) { p1+p2+p3; Es musste für jede Methode mit einer bestimmten Anzahl Argumente eine eigene Implementierung zur Verfügung gestellt werden. Die andere Möglichkeit war, das Problem mit einem Array als Argument zu lösen. Sie bringt den gewünschten Vorteil einer einzigen Methoden-Implementierung mit sich. Der Code für die Methode sum sieht dazu wie folgt aus: int sum( int[] p ) { int result = 0; for( int i : p ) result += i; return result; } Jedoch muss für die Verwendung dieser Methode zuerst ein entsprechendes Array definiert werden, was lästig ist und unschön wirkt. Java 1.5 bietet nun die Möglichkeit, eine Methode mit variablen Argumenten, kurz Varargs, zu definieren. So muss nur eine einzige Methode bereitgestellt werden, welche für eine beliebige Anzahl Argumente verwendet werden kann. 6.2 Die neue Syntax Um eine Methode mit einer variablen Anzahl Argumente zu definieren, steht als Parameter der zu übergebene Typ gefolgt von drei Punkten ( ... ) und dem Namen der Variable. Die Methode sum wir damit folgendermassen definiert: 14 - 19 Syntactic Sugar int sum( int... p ) { int result = 0; for( int i = 0; i < p.length; i++ ) result += p[i]; return result; } Die Methode kann nun für jede beliebige Anzahl Argumente verwendet werden, also beispielsweise mit drei Argumenten: sum(3, 6, 9). Die Methode kann auch ohne Parameter aufgerufen werden. Im obigen Beispiel liefert sie dann 0 zurück. 6.3 Hintergründe Die Anzahl Argumente wird im Rumpf der Methode mit dem Attribut length abgefragt und die einzelnen Argumente mit einem Index in eckigen Klammern angesprochen, was beides auf ein Array hindeutet. Und genau das ist es auch: Der Compiler schreibt den Methodenaufruf einer Varargs-Methode in einen Methodenaufruf mit einem Array als Argument um. Die beiden Aufrufe der Methode sum sum( 3, 6 ); sum( 3, 6, 9 ); werden vom Compiler in die Form sum( new int[] { 3, 6 } ); sum( new int[] { 3, 6, 9 } ); umgeschrieben. Die Java Virtual Machine musste also auch für die Varargs nicht geändert werden. Zu beachten ist, dass immer nur das letzte übergebene Argument ein variables Argument sein darf. Dies bedeutet gleichzeitig, dass nur höchstens ein Argument einer Methode ein variables Argument sein kann. Im Umgang mit Varargs ist das Überladen mit Methoden speziell zu beachten: Folgende zwei Methodendeklarationen sind nicht gleichzeitig erlaubt und führen zu einem Compilerfehler: int sum(int a, int... b){…} int sum(int a, int[] b){…} Der Grund dafür ist, dass der Compiler die erste Signatur so umschreibt, dass sie zur zweiten nicht mehr unterschieden werden kann, und so nicht weiss, welche von beiden aufgerufen werden soll. Ein zweiter Spezialfall ist der folgende Code. Er zeigt die Methode sum, welche einmal mit Varargs und einmal mit einer konventionellen Signatur deklariert ist: int sum(int a, int b){…} int sum(int a, int... b){…} 15 - 19 Syntactic Sugar Wird nun die Methode mit sum(1,2) aufgerufen, so stellt sich die Frage, welche Methode nun Aufgerufen wird. Da der Compiler immer die spezifischste Methode aufruft, würde hier die zweite Methode zum Einsatz aufgerufen. 6.4 Die Methode printf Eine Anwendung dieser neuen Möglichkeit ist die Methode printf der Klasse java.io.PrintStream und java.io.PrintWriter. Diese Methode wurde mit einigen Änderungen von der Programmiersprache C übernommen. Sie dient dazu, formatierte Strings auszugeben und tut dies mit Hilfe von Varargs. Das erste Argument der printfMethode ist ein Format-String. Ein Format-String ist ein String, welcher neben den auszugebenden Zeichen auch so genannte Format-Spezifizierer (format specifier) enthält. Diese sind Stellvertreter für Argumente und geben an, wie diese Argumente formatiert werden sollen. Die Anzahl dieser Format-Spezifizierer ist beliebig. Die weiteren Argumente der Methode printf sind Werte, die anstelle der Format-Spezifizierer in den String eingesetzt werden. Der Code zeigt die printf-Methode mit zwei Format-Spezifizierer, welche in der Ausgabe mit den darauf folgenden int-Argumenten ersetzt werden: System.out.printf(“Ich wähle die Zahlen %d und %d!“, 3, 6 ); Als Ausgabe erscheint der folgende String: Ich wähle die Zahlen 3 und 6! Die Format-Spezifizierer %d werden im String mit dem zweiten und dritten Argument ersetzt. Dabei gibt das Zeichen % an, dass jetzt ein Platzhalter folgt und d gibt an, dass dies dezimale int-Werte sind. Die Formatangabe kann durch mehrere Parameter sehr genau festgelegt werden. Die Syntax für einen Format-Spezifizierer sieht wie folgt aus: %[argument_index$][flags][width][.precision]conversion Dabei sind die in eckigen Klammern geschriebenen Angaben optional. Die im obigen Beispiel verwendete Formatangabe %d legt demnach nur die conversation fest. Sie bestimmt die eigentliche Formatierung und ist vom Datentyp des Argumentes abhängig. Die Angabe argument_index legt die Position in der Liste der gefolgten Argumente fest. Das erste Argument nach dem String-Argument wird mit 1$ angesprochen, das zweite mit 2$, usw. Die Angabe flag modifiziert den Ausgabe-Parameter, fügt zum Beispiel ein Padding hinzu. Er ist abhängig von der gewählten conversion. Die Angabe width gibt an, wie viele characters im Minimum ausgegeben werden. Die Angabe precision ist ebenfalls von der conversion abhängig. Meist gibt sie die maximale Anzahl characters an, die ausgegeben werden. Genauere Angaben zu den Format-Spezifizierern sind in der Java 1.5 API von Sun unter java.util.Formatter zu finden. 16 - 19 Syntactic Sugar 6.5 Anwendung Die alte Syntax, in welcher ein Array nach einem Format-String mitgegeben wurde, sollte immer durch Varargs ersetzt werden, sofern das Array nicht schon in einem vorigen Kontext verwendet wurde. Es soll also vermieden werden, speziell für einen Methodenaufruf mit variablen Argumenten ein Array zu definieren. Grundsätzlich sollten die Varargs verwendet werden, sofern dies sinnvoll und auch möglich ist. Man muss sich dabei im Klaren sein, dass aus der Definition nicht mehr erkannt werden kann, in welcher Form, also mit welcher Anzahl Argumente die Methode verwendet wird. Die Methode printf bietet viele Möglichkeiten, einen Text formatiert auszugeben. Jedoch ist in den Formatangaben nicht auf den ersten Blick zu erkennen, wie der Text nun ausgegeben wird. 7 Schlusswort Auch wenn der syntaktische Zucker nicht die wichtigste Neuerung in Java 5 ist, so ist er eine sehr hilfreiche. Die Neuerungen in diesem Bereich erleichtern das Schreiben wie auch das Lesen des Programmcodes und ermöglichen so ein effizienteres Programmieren in Java. Solange aber der Upgrade auf Java 5 noch nicht überall erfolgt ist, müssen auch diese Neuerungen mit Vorsicht eingesetzt werden. Ist dies aber geschehen, so gibt es keinen Grund mehr, die neue Syntaxerweiterung von Java 5 zu meiden. 17 - 19 Syntactic Sugar Bibliographie For-Each Loop: Sun Microsystems, Inc. (2004). The For-Each Loop. http://java.sun.com/j2se/ 1.5.0/docs/guide/language/foreach.html Stand: 08.03.2005 Java Community Process (JCP), Sun Microsystems, Inc. (2000 - 2001). An enhanced for loop for the Java Programming Language. http://www.jcp.org/ aboutJava/communityprocess/jsr/tiger/enhanced-for.html Stand: 08.03.2005 Autoboxing: Sun Microsystems, Inc. (2004). Autoboxing. http://java.sun.com/j2se/1.5.0/ docs/guide/language/autoboxing.html enhanced-for.html Stand: 09.03.2005 Nowak, Johannes (2005). Fortgeschrittene Programmierung mit Java 5. Heidelberg, 1. Auflage. Kap. 2.1 Autoboxing und Unboxing Static Import: Sun Microsystems, Inc. (2004). Static Import. http://java.sun.com/j2se/1.5.0/ docs/guide/language/static-import.html Stand: 11.03.2005 Nowak, Johannes (2005). Fortgeschrittene Programmierung mit Java 5. Heidelberg, 1. Auflage. Kap. 2.4 Statische Importe Typesafe Enums: Friedrich Esser (2004 - 2005). Enumerationen in Java 5. BuchUpdate von „Das Tiger-Release: Java 5 im Einsatz“. http://www.galileocomputing.de/download/dateien/ 611/enumerationen_java.pdf Stand: 15.03.2005 Sun Microsystems, Inc. (2004). Enums. http://java.sun.com/j2se/1.5.0/docs/ guide/language/enums.html Stand: 15.03.2005 Nowak, Johannes (2005). Das Tiger-Release: Java 5 im Einsatz. Heidelberg, 1. Auflage. Kap. 2.3 Enums Java Community Process (JCP), Sun Microsystems, Inc. (2000 - 2001). A Typesafe Enum Facility for the Java Programming Language. http://www.jcp.org/ aboutJava/communityprocess/jsr/tiger/enum.html Stand: 15.03.2005 18 - 19 Syntactic Sugar Varargs: Nowak, Johannes (2005). Auflage. Kap. 2.5 Varargs Das Tiger-Release: Java 5 im Einsatz. Heidelberg, 1. Sun Microsystems, Inc. (2004). Enums. http://java.sun.com/j2se/1.5.0/docs/ guide/language/varargs.html Stand: 16.03.2005 Sun Microsystems, Inc. (2004). java.util.Formatter, API Specifications J2SE 1.5.0. http://java.sun.com/j2se/1.5.0/docs/api/ Stand: 19.03.2005 Christian Ullenboom (2004). Java ist auch eine Insel, Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release). Online Openbook, Version 4, Kap. 4.7 Formatieren von Ausgaben http://www.galileocomputing.de/openbook/javainsel4/ javainsel_04_006.htm Stand: 17.03.2005 19 - 19