Enumerationen
Eigenschaften von Enumerationen
- Können nicht von anderen Klasse ableiten, sondern werden vom Compiler automatisch von
Enum
abgeleitet - Können Felder, Konstanten und Methoden enthalten
- Enum-Konstanten sind echte Java-Objekte
- Enum-Konstanten sind Singletons
- es gibt zu jeder Enum-Konstante immer nur ein einziges Objekt
- dürfen mit
==
verglichen werden - können nicht mit
new
erzeugt werden - können nur
private
oder default-sichtbare Konstruktoren haben - Enum-Konstanten können statisch importiert werden
Alle Enumerationen sind automatisch von java.lang.Enum
abgeleitet. Daher kann man in der Deklaration einer Enumeration auch kein extends
angeben. Die Klasse heißt Enum und nicht Enumeration
, weil es bei ihrer Einführung bereits eine Klasse mit dem Namen Enumeration
gab und man diesen daher nicht mehr verwenden konnte.
Der Konstruktor einer Enumeration muss private oder default-Sichtbarkiet haben. Dies ist nötig, da andernfalls die Eigenschaft der Konstanten als Singleton unterwandert würde.
Wie man an dieser Aufzählung bereits sieht, gehen die Eigenschaften von Java-Enumerationen weit über das hinaus, was in anderen Programmiersprachen möglich ist. So werden z. B. in C# und C++ Enumerationen einfach auf int-Werte abgebildet, wohingegen in Java das sog. „type safe Enumeration Pattern“ mit echten Objekten und einem reichhaltigen Satz von Features zur Verfügung steht.
Einige der Möglichkeiten, die Enumerationen bieten sind obskur, so die Möglichkeit abstrakte Methoden zu definieren (wird später noch erläutert). Man muss – und sollte – diese aber nicht unbedingt verwenden. Davon abgesehen sind Enumerationen sehr nützlich.
Beispiel: Enum mit Konstruktor
public enum Planet {
MERKUR(59e+6, 88), VENUS(108e+6, 225),
ERDE(150e+6, 365), MARS(228e+6, 1.9*365),
JUPITER(778e+6, 11.9*365), SATURN(1427e+6, 29.5*365),
URANUS(2870e+6, 84*365), NEPTUN(4497e+6, 165*365);
private final double entfernungsonne; // in km
private final double umlaufzeit; // in tagen
private Planet(double entfernungsonne, double umlaufzeit) {
this.entfernungsonne = entfernungsonne;
this.umlaufzeit = umlaufzeit;
}
public double bahngeschwindigkeit() {
return 2 * entfernungsonne * Math.PI / umlaufzeit / 24;
}
}
Man kann einer Enumeration Daten mitgeben, d. h. die Konstanten tragen dann neben ihrem Namen noch beliebige weitere Informationen. Dies kann sehr nützlich sein, wenn es Werte gibt, die direkt zu den Konstanten gehören. Im Beispiel sind das die Bahngeschwindigkeit der Planeten und ihre Entfernung zur Sonne.
Die Enum-Konstanten (z. B. MERKUR
) geben dann bei ihrer Deklaration die Werte an, die in den Attributen des Objektes gespeichert werden sollen. Technisch gesehen handelt es sich also um einen Konstruktoraufruf, nur dass das new
implizit erzeugt wird.
Beispiel: Iterieren über eine Enum
public class Ausgabe {
public static void main(String[] args) {
for (Planet planet : Planet.values()) {
System.out.printf("%s -> %,.0f km/h%n",
planet.name(),
planet.bahngeschwindigkeit());
}
}
}
MERKUR -> 175.525 km/h VENUS -> 125.664 km/h
ERDE -> 107.589 km/h MARS -> 86.071 km/h
JUPITER -> 46.893 km/h SATURN -> 34.696 km/h
URANUS -> 24.506 km/h NEPTUN -> 19.549 km/h
Jede Enumeration hat eine statische Methode values()
, die ein Array mit allen vorhandenen Konstanten zurückliefert. Will man also ein Programm schreiben, dass über alle Konstanten iteriert und dann nicht verändert werden muss, wenn man eine weitere Konstante einführt, bietet sich diese Methode an. Über name()
kann man dann den Namen der Konstante wieder auslesen.
Das Beispiel nutzt genau dies aus. Es läuft mit einer for-each-Schleife über alle Konstanten, liest dann den Namen aus und ruft eine Methode (bahngeschwindigkeit()
) auf.
Beispiel: Enums und if / switch
public class SpaceTravel {
private static final Planet HOME_PLANET = Planet.ERDE;
public void travel(Planet zielPlanet) {
if (zielPlanet == HOME_PLANET) {
return; // wir sind doch schon da
}
switch (zielPlanet) {
case JUPITER: // langer Flug
break;
case MARS: // kurzer Flug
break;
...
}
}
}
Da es sich bei Enumerationen um Singletons handelt, darf man sie – anders als alle anderen Objekte in Java – mit ==
vergleichen. Zusätzlich besteht noch die Möglichkeit, sie in switch-Verzweigungen zu verwenden.
Beachtenswert ist, dass man bei den case-Statements nur den Namen der Konstante ohne vorangestellten Namen der Enumeration benutzen muss. Der Grund liegt darin, dass bereits aufgrund des Typs der Variable im switch
klar ist, um welche Enumeration es sich handelt (zielPlanet
ist vom Typ Plant
, damit ist klar, wo der Compiler nach JUPITER
und MARS
suchen muss).
Wichtige Methoden von Enumerationen
public final String name()
– Liefert den Namen der Konstante, genauso wie sie in der Datei angegeben wurdepublic final int ordinal()
– Liefert die Ordinalzahl der Konstante (sollte normalerweise nicht verwendet werden)public String toString()
– Liefert den Namen der Konstante, kann überschrieben werden, um besser lesbare Namen zurückzugebenpublic static Enum valueOf(String name)
– Wandelt den Namen der Konstanten (wie vonname()
zurückgeliefert) wieder in eine Enum umpublic static Enum[] values()
– Gibt ein Array mit allen vorhandenen Enum-Konstanten zurück
public final int compareTo(Enum o)
– Vergleicht Enums anhand ihrer Ordinalzahlenpublic final boolean equals(Object other)
– EntsprichtObject.equals()
und damit==
public final int hashCode()
– EntsprichtObject.hashCode()
protected final Object clone()
– Darf nicht verwendet werden, da Enums Singletons sind
Da alle Enumerationen von der Klasse Enum
abgeleitet sind, erben sie entsprechende Methoden. Die meisten Methoden sind sofort verständlich. Wichtig ist der Unterschied zwischen name()
und toString()
. Beide liefern standardmäßig den Namen der Konstante, wie er in der Enumeration deklariert wurde, zurück. toString()
kann man aber überschreiben, name()
nicht. Insofern kann man toString()
verwenden, um einen eigenen Namen zurückzugeben während name()
immer unverändert bleibt und exakt den Namen der Konstante entsprechend ihrer Deklaration zurück gibt.
Da es sich bei Enumerationen immer um Singletons handelt, sollten die Methoden hashCode()
, equals()
, clone()
und compareTo()
nicht verändert werden. Deshalb sind sie als final
deklariert, um ein versehentliches Überschreiben zu verhindern.
Beispiel: Methoden auf Enums
String name = Planet.ERDE.name();
Planet planet = Planet.valueOf(name);
System.out.printf("Wir leben auf der %s, sie ist " +
"der %d Planet%n", planet, planet.ordinal() + 1);
System.out.printf("Wir haben %d Planeten im Sonnensystem%n",
Planet.values().length);
Wir leben auf der ERDE, sie ist der 3 Planet
Wir haben 8 Planeten im Sonnensystem
Das Beispiel verwendet die eingebauten Methoden, die jede Enumeration hat. Es wird gezeigt, dass man den Namen einer Konstante (Planet.ERDE.name()
) über die statische valueOf()
-Methode wieder in eine Referenz auf die Konstante umwandeln kann.
Man sieht auch, dass die von ordinal()
gelieferten Werte bei 0 beginnen, sodass im Beispiel noch eine 1 addiert werden muss.
Abstrakte Methoden
- Enums können abstrakte Methoden enthalten
- Diese Methoden müssen für jede Enum-Konstante implementiert werden
- Mit dieser Konstruktion sollen unübersichtliche case bzw. if-Konstrukte vermieden werden
Die Möglichkeit in einer Enumeration abstrakte Methoden zu definieren, um diese dann in den Konstanten zu implementieren, ist ausgesprochen obskur und verwirrend. Wenn man sich Enumerationen als Klassen und die Konstanten als Objekte vorstellt bedeutet dies, dass die Objekte (sic!) die Methoden implementieren, die die Klasse als abstrakt definiert hat. Aus objektorientierter Sicht ist dies unsinnig, da nur Klassen, aber nicht Objekte Methoden tragen können.
Technisch löst Java diesen Widerspruch indem für jede Konstante eine eigene Subklasse der Enumeration generiert wird und diese dann die entsprechende Methodenimplementierung enthält.
Wegen seiner seltsamen Syntax sollte man dieses Feature nur einsetzen, wenn es einen wirklichen Vorteil bringt.
Beispiel: Abstrakte Methoden
public enum Operation {
ADDITION {
public double anwenden(double op1, double op2) {
return op1 + op2;
}
},
SUBTRAKTION {
public double anwenden(double op1, double op2) {
return op1 - op2;
}
},
MULTIPLIKATION {
public double anwenden(double op1, double op2) {
return op1 * op2;
}
};
public abstract double anwenden(double op1, double op2);
}
Das Beispiel zeigt, wie die Konstante die abstrakte Methode anwenden
implementieren. Abhängig von der Konstante führt die Methoden unterschiedliche Rechenoperationen durch.
public class Rechner {
public double rechne(double op1, double op2, Operation operation) {
return operation.anwenden(op1, op2);
}
public static void main(String[] args) {
Rechner r = new Rechner();
double summe = r.rechne(5, 5, Operation.ADDITION);
double differenz = r.rechne(6, 3, Operation.SUBTRAKTION);
double produkt = r.rechne(6, 3, Operation.MULTIPLIKATION);
System.out.printf("%.0f, %.0f, %.0f", summe, differenz, produkt);
}
}
10, 3, 18