Advanced Java Services | Enums |
Es hat lange gedauert, bis die Java-Entwickler sich entschlossen haben Enumerationen in Java einzuführen. Lange Zeit hielt man sie für überflüssig, doch durch die Konkurrenz von C# hat man sich nun doch durchgerungen Enumerationen einzuführen. Im Gegensatz zu C++ und C# steht das in 1.5 bzw. 5.0 eingeführte Schlüsselwort enum für eine spezielle Form einer Klasse. Die Enum-konstanten sind damit nicht einfach primitive int- Konstanten wie in anderen Sprachen, sondern Objekte vom Typ der enum-Klasse. Schauen wir uns zum Vergleich mal Enumerationen in C/C++ an:
enum Days { Mon, Tue, Wed, Thu, Fri, Sat, Sun } ;
Mit der obigen Vereinbarung wird ein neuer Datentyp Days geschaffen. Dieser hat einen Wertebereich, der aus den sieben Konstanten besteht, die in der Klammer aufgeführt werden. Hinter den Konstanten verbergen sich Zahlen vom Typ int, die Zählung ist fortlaufend und beginnt bei 0. Wenn man will, kann man die Zählung auch mit einer anderen Zahl beginnen lassen:
enum Days { Mon=1, Tue, Wed, Thu, Fri, Sat, Sun } ;
Die Verwendung des Typs Days kann etwa so aussehen:
int main(int argc, char *argv[]) { Days day = Sat ; cout << Tue << endl; // Ausgabe: 2 cout << Sat << endl; // Ausgabe: 6 int x = Sat; // OK short y = Sun; // OK unsigned z = Mon; // OK //day = x ; // NOK: invalid conversion }
Um obige main-Funktion zum Laufen zu bringen braucht man natürlich noch ein include und die Angabe eines namespace. Man sieht deutlich, daß hier enums im wesentlichen integer-Konstanten sind. Eine Typprüfung findet statt, der Compiler erlaubt eine Zuweisung nur in einer Richtung. Die letzte auskommentierte Zuweisung ist ja auch gefährlich, könnte man so der Variablen day auch Werte zuweisen, die garnicht im Wertebereich von Days liegen.
In Java definiert man mit enum eine spezielle Form einer Klasse. Es zeigt sich hier wieder einmal, was für
ein flexibler Begriff eine Klasse ist und was man damit alles modellieren kann. Damit geht man in Java
einen großen Schritt weiter, denn in den anderen Sprachen sind Enumerationen lediglich, wie es so schön
heißt "glorified integers".
Dabei sieht es zunächst fast genauso aus wie im obigen Beispiel:
enum Season { SPRING, SUMMER, AUTUMN, WINTER ; }
In unserem enum Season werden vier Konstanten vereinbart. Hier ist jeder gültige Bezeichner zulässig. Die Verwendung des Typs Season kann etwa so aussehen:
public class EnumDemo1 { public static void main(String args[]) { // Anlegen eines Objekts Season seas = Season.SPRING ; // Verwendung der Methoden name() und ordinal() : System.out.println("seas.name() = " + seas.name()); // SPRING System.out.println("seas.ordinal() = " + seas.ordinal()); // 0 System.out.println("Season.SUMMER.name() = " + Season.SUMMER.name()); // SPRING System.out.println("Season.SUMMER.ordinal() = " + Season.SUMMER.ordinal()); // 0 // Verwenden der Methode valueOf() Season sea = Season.valueOf("AUTUMN"); System.out.println(sea) ; // Verwenden der Methode values() Season[] seasons = Season.values(); for(Season s : seasons) System.out.println(s) ; } }
Im obigen Beispiel werden vier Methoden verwendet, name(), ordinal(), valueOf() und values(). Die Methoden
name() und ordinal() findet man in der API unter der Klasse Enum, was darauf hindeutet, daß unser
enum Season "irgendwie" irgendwie mit der Klasse Enum verwandt ist. Die Methode valueOf() existiert ohne daß
wir sie geschrieben haben. Es gibt zwar in Enum eine Methode gleichen Namens, aber mit einer anderen Signatur.
Noch merkwürdiger verhält es sich mit der Methode values(). Sie taucht nicht in der API auf und wir haben sie auch
nicht codiert.
Offensichtlich generiert der Compiler im Falle eines enums einiges im Hintergrund. Auf diese Weise nimmt uns der
Compiler einerseits Arbeit ab, erzwingt aber dadurch andrerseits gewisse Regeln, nach denen wir uns
richten müssen.
Der Compiler compiliert unser enum Season zu einer Klasse Season.class . Schaut man sich mit Hilfe von Reflection
oder einfacher mit einem Decompiler an, was der Compiler aus diesen drei Zeilen macht, so wird der Code von EnumDemo1
verständlich (und der Unterschied zu C/C++ oder C# deutlich):
final 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 = new Season("SPRING", 0); SUMMER = new Season("SUMMER", 1); AUTUMN = new Season("AUTUMN", 2); WINTER = new Season("WINTER", 3); VALUES = new Season[] { SPRING, SUMMER, AUTUMN, WINTER } ; } private Season(String s, int i) { super(s, i); } public static final Season[] values() { return (Season[])VALUES.clone(); } public static Season valueOf(String s) { return (Season)Enum.valueOf(Season, s); } }
Die Enumkonstanten werden also als statische Konstanten angelegt und in einem static initializer ins Leben herufen. Zusätzlich wird ein statisches Array angelegt. Ein clone dieses Arrays wird über die statische Methode values() zurückgeliefert. Diese Methode wird vom Compiler automatisch generiert. Ebenso automatisch generiert wird die zweite statische Methode valueOf(), die sich auf die namensgleiche statische Methode valueOf() aus der Klasse Enum stützt. Da die Elternklasse Enum nur über den protected Konstruktor Enum(String name, int ordinal) verfügt, muß es in der Ableitung einen Konstruktor geben, der einen entsprechenden super-Aufruf enthält. Dieser Konstruktor wird als private-Konstruktor generiert, d.h. in Falle eines enum generiert der Compiler keinen default-Konstruktor sondern einen Konstruktor mit zwei Parametern:
private Season(String s, int i) { super(s, i); }
Wir fassen unsere Erkenntnisse wie folgt zusammen
Die Klasse Enum, von der sich jedes selbstgeschriebene enum automatisch ableitet verfügt nur über einen einzigen Konstruktor
protected Enum(String name, int ordinal)
Die Klasse Enum besitzt damit insbesondere keinen Defaultkonstruktor. Da man in einer selbstgeschriebenen enum-Klasse keinen Konstruktor schreiben muß erstellt also der Compiler einen Konstruktor. Das ist bei jeder Klasse ohne Konstruktor der Fall, hier allerdings generiert der Compiler keinen Defaultkonstruktor. Da der einzige Konstruktor der Elternklasse Enum wie bereits erwähnt zwei Parameter hat, muß der Compiler einen Aufruf dieses speziellen Elternkonstruktors implementieren. Der Compiler löst das Problem indem er einen Konstruktor mit zwei Parametern anlegt und die beiden Parameter dann nach oben durchreicht (siehe die decomplilierte Klasse oben).
private Season(String s, int i) { super(s, i); }
Es fällt weiter auf, daß dieser Konstruktor private angelegt wird.
Wir variieren nun unser Seasonbeispiel und schreiben selbst Konstruktoren. Wir wollen die automatische
Numerierung der Konstanten ändern.
enum Season2 { SPRING(0.5), SUMMER(1), FALL(1.5), WINTER(2) ; Season2(double d) // public, protected not allowed { System.out.println("Season constructor Season(double d) creates " + this.name() ) ; } }
Hierzu schreiben wir ein sehr kurzes main:
public class EnumDemo2 { public static void main(String args[]) { Season2 sea = Season2.SUMMER; } }
Überlegen Sie, aus wievielen Zeilen der output dieses Programms besteht, bevor Sie es starten.
Auch diesmal interessiert uns wieder, was der Compiler im Hintergrund so alles anstellt. Hier
ein Blick hinter die Kulissen:
final class Season2 extends Enum { public static final Season2 SPRING; public static final Season2 SUMMER; public static final Season2 FALL; public static final Season2 WINTER; private static final Season2 $VALUES[]; static { SPRING = new Season2("SPRING", 0, 0.5D); SUMMER = new Season2("SUMMER", 1, 1.0D); FALL = new Season2("FALL", 2, 1.5D); WINTER = new Season2("WINTER", 3, 2D); $VALUES = (new Season2[] { SPRING, SUMMER, FALL, WINTER }); } public static Season2[] values() { return (Season2[])$VALUES.clone(); } public static Season2 valueOf(String s) { return (Season2)Enum.valueOf(Season2, s); } private Season2(String s, int i, double d) { super(s, i); System.out.println((new StringBuilder()). append("Season constructor Season(double d) creates ").append(name()).toString()); } }
Interessant ist hier der generierte Konstruktor: die Signatur "Season2(double d)" wird vom Compiler umgesetzt zu "private Season2(String s, int i, double d)". Durch Ausprobieren kann man feststellen, daß die modifier "public" und "protected" bei einem Konstruktor vom Compiler nicht zugelassen werden. Mehr noch, der vom Compiler generierte Konstruktor erhält immer den modifier "private". Es schadet nicht zu bemerken, daß kein weiterer Konstruktor generiert wird, insbesondere "fehlt" der Quasidefaultkonstruktor mit der Signatur "private Season2(String s, int i)". Wir fassen wieder zusammen:
Von einem Singleton spricht man, wenn eine Klasse so konzipiert wird, daß man nur ein einziges Objekt anlegen kann. In solchen Klassen ist der Konstruktor private, stattdessen gibt es eine statische Methode zur Objekterzeugung. Mit dieser kann man sicherstellen, daß auch bei einem wiederholten Aufruf der statischen Methode immer dasselbe Objekt geliefert wird. Man kann eine selbstentworfene enum-Klasse als Variante dieses Musters auffassen. Mit der Wahl der Anzahl der Konstanten eines enum legt man die Anzahl der Objekte fest die zu einer enum-Klasse gehören. Es gibt dann nur genau diesen (konstanten) Satz von konstanten Objekten. Bei der ersten Verwendung irgendeines der Objekte aus diesem Vorrat wird sogleich der ganze Satz in einem statischen Initialisierer mit Hilfe des privaten Konstruktors generiert und damit ist die Objekterzeugung beendet.
Das folgende Beispiel zeigt eine ganze Reihe von weiteren Eigenschaften und Möglichkeiten.
Wir entwerfen ein enum Borders innerhalb der Klasse EnumDemo3.
Die enum-Klasse Borders hat einen privaten Datenteil (private int size), in dem die Breite eines Randes gespeichert wird.
Beim Anlegen der Konstanten kann über eine runde Klammer ein Defaultwert übergeben werden, hier etwa LEFT(6) usw.
Dieser Wert wird dann in size abgelegt. Damit das funktioniert müssen wir aber auch einen passenden Konstruktor
schreiben, der Compiler kann ihn nicht erzeugen. Der Konstruktor darf weder public noch protected sein. Er wird intern
dazu verwendet, die in den runden Klammern angegebenen Defaultwerte zu realisieren. Wir möchten die Dicke der Ränder aber auch
nachträglich noch ändern können und schreiben deshalb ein set/get-Methodenpaar. Des weiteren legen wir unser enum diesmal
innerhalb einer Klasse (EnumDemo3) an und demonstrieren damit, daß eine enum auch in der Form einer nested class auftreten kann.
Natürlich müssen Enumtypen zusammen mit switch verwendet werden können. Das blau eingefärbte switch-Konstrukt
verarbeitet der Compiler ohne zu klagen. Hier wird mit einem Trick gearbeitet. Über die Methode ordinal() verschafft sich
der Compiler doch wieder int-Zahlen und erzeugt so ein normales switch-Konstrukt. Das sieht ungefähr so aus:
final int left = Borders.LEFT.ordinal(); switch( bor.ordinal() ) // das geht noch { case Borders.LEFT.ordinal() : System.out.println("left border"); break; // Compilermeldung: constant expression required case Borders.RIGHT.ordinal() : System.out.println("right border"); break; // Compilermeldung: constant expression required default: System.out.println("neither left nor right border"); }
Dies kann man aber nicht selbst nachbilden, man beachte die Fehlermeldung im Kommentar.
public class EnumDemo3 { private enum Borders { LEFT(6), TOP(8), RIGHT(6), BOTTOM(4) ; private int size ; // notwendig! Borders(int a) { size = a ; } public int getSize() { return size ; } public void setSize(int w) { size = w ; } } public static void main(String args[]) { //Borders bo = new Borders(); enum types may not be instantiated Borders bor = Borders.TOP ; System.out.println(bor.getSize() ); bor.setSize(10); System.out.println(bor.getSize() ); switch(bor) { case LEFT : System.out.println("left border"); break; case RIGHT : System.out.println("right border"); break; default: System.out.println("neither left nor right border"); } } }
Aufmerksamen Lesern wird aufgefallen sein, daß die in main() rot hervorgehobene Zeile keine Fehlermeldung erzeugt. Folgerung: Innere enum-Klassen werden als "static nested" angelegt. Ein Blick auf das Decompilat bestätigt dies:
private static final class EnumDemo3$Borders extends Enum { public static final EnumDemo3$Borders LEFT; public static final EnumDemo3$Borders TOP; public static final EnumDemo3$Borders RIGHT; public static final EnumDemo3$Borders BOTTOM; private static final EnumDemo3$Borders $VALUES[]; static { LEFT = new EnumDemo3$Borders("LEFT", 0, 6); TOP = new EnumDemo3$Borders("TOP", 1, 8); RIGHT = new EnumDemo3$Borders("RIGHT", 2, 6); BOTTOM = new EnumDemo3$Borders("BOTTOM", 3, 4); $VALUES = (new EnumDemo3$Borders[] { LEFT, TOP, RIGHT, BOTTOM }); } public static final EnumDemo3$Borders[] values() { return (EnumDemo3$Borders[])$VALUES.clone(); } public static EnumDemo3$Borders valueOf(String s) { return (EnumDemo3$Borders)Enum.valueOf(EnumDemo3$Borders, s); } private int size; private EnumDemo3$Borders(String s, int i, int j) { super(s, i); size = j; } public int getSize() { return size; } public void setSize(int i) { size = i; } }
Zusammenfassung:
In Vorbereitung