6.4 Konstanten und Aufzählungen
In Programmen gibt es Variablen, die sich ändern (wie zum Beispiel ein Schleifenzähler), aber auch andere, die sich beim Ablauf eines Programms nicht ändern. Dazu gehören etwa die Startzeit der Tagesschau oder die Maße einer DIN-A4-Seite. Die Werte sollten nicht wiederholt im Quellcode stehen, sondern über ihre Namen angesprochen werden. Dazu werden Variablen deklariert, denen genau der konstante Wert zugewiesen wird; die Konstanten heißen dann symbolische Konstanten.
In Java gibt es zur Deklaration von Konstanten zwei Möglichkeiten:
Selbst definierte öffentliche statische finale Variablen nehmen konstante Werte auf.
Aufzählungen über ein enum (die intern aber auch nur öffentliche finale statische Werte sind)
6.4.1 Konstanten über statische finale Variablen
Statische Variablen werden auch verwendet, um symbolische Konstanten zu deklarieren. Damit die Variablen unveränderlich bleiben, gesellt sich der Modifizierer final hinzu. Dem Compiler wird auf diese Weise mitgeteilt, dass dieser Variablen nur einmal ein Wert zugewiesen werden darf. Für Variablen bedeutet dies: Es sind Konstanten; jeder spätere Schreibzugriff wäre ein Fehler. In der Regel sind die Konstanten öffentlich, aber natürlich können sie auch privat sein, wenn sie nur die Klasse etwas angehen.
Konstante Werte haben wir schon bei GameUtils eingesetzt:
public class GameUtils {
...
public static final int MAX_ID_LEN = 20 /* chars */;
...
}
Da im Quellcode das Vorkommen von Zahlen wie der 20 undurchsichtig wäre, sind symbolische Namen zwingend. Stehen dennoch Zahlen ohne offensichtliche Bedeutung im Quellcode, so werden sie magische Zahlen (engl. magic numbers) genannt. Es gilt, diese Werte in Konstanten zu fassen und sinnvoll zu benennen.
[+] Tipp
Es ist eine gute Idee, die Namen von Konstanten durchgehend großzuschreiben, um ihre Bedeutung hervorzuheben.
Der Zugriff auf die Variablen sieht genauso aus wie ein Zugriff auf andere statische Variablen.
[zB] Beispiel
Greife auf Konstanten zurück:
System.out.println( Math.PI );
int len = GameUtils.MAX_ID_LEN;
if ( s.length() > GameUtils.MAX_ID_LEN )
System.out.println( "Zu lang!" );
6.4.2 Typunsichere Aufzählungen
Konstanten sind eine wertvolle Möglichkeit, um den Quellcode aussagekräftiger und klarer zu gestalten, was wichtig ist, denn Quellcode wird öfter gelesen als geschrieben. Oftmals finden sich Konstanten für mathematische Konstanten oder Größenbeschränkungen.
Eine besondere Form bilden Konstanten, wenn sie als Elemente von Aufzählungen verwendet werden. Aufzählungen erinnern an abgeschlossene Mengen, so wie:
Tage der Woche (Montag, Dienstag …)
Monate eines Jahrs (Januar, Februar …)
Font-Stile (fett, kursiv …)
vordefinierte Linienmuster (durchgezogen, gestrichelt …)
Die Tage der Woche und auch die Monate des Jahres werden zum Beispiel von der Klasse java. util.Calendar über öffentliche statische int-Konstanten angeboten.
[zB] Beispiel
Die Frau wird im Juni schwanger. Wann müssen Windeln gekauft werden?
int month = Calendar.JUNE;
int conception = (month + 9) % 12;
System.out.println( conception ); // 2
Soll eine Klasse Materials zum Beispiel Konstanten für die Beschaffenheit eines Materials deklarieren, kann das so aussehen:
public class Materials {
private Materials() { } // Privater Konstruktor
public static final int SOFT = 0;
public static final int HARD = 1;
public static final int DRY = 2;
public static final int WET = 3;
public static final int SMOOTH = 4;
public static final int ROUGH = SMOOTH + 1;
}
Für ihre Belegungen ist es günstig, die Konstanten relativ zum Vorgänger zu wählen, um das Einfügen in der Mitte zu vereinfachen. Das sehen wir bei der letzten Variablen, ROUGH.
Problem mit dem Datentyp int als Konstantentyp
Einfache Konstantentypen – wie bei uns int – bringen den Nachteil mit sich, dass die Konstanten nicht unbedingt von jedem angewendet werden müssen und ein Programmierer die Zahlen oder Zeichenketten eventuell direkt einsetzt. Das ist in dem Moment problematisch, sobald sich die Belegung einer Konstanten einmal ändert. Bei einer Konstanten wie Math.PI wird das nicht passieren, aber die Maximallänge eines Strings kann durchaus einmal angepasst werden, genauso wie der Materialtyp, wenn zum Beispiel ein neues Material eingeschoben wird.
Im Idealfall haben Aufzählungen einen eigenen Typ. Sind sie »nur« vom Typ int, führt das leicht zu Fehlern. Die Font-Klasse der Java-API illustriert das Problem: Die Parametertypen beim Konstruktor sind: Font(String, int, int). Versagt unser Gedächtnis, für was welches int steht, heißt es im Aufruf vielleicht:
Font f = new Font( "Dialog", 12, Font.BOLD );
Leider ist dies falsch, denn die Argumente für die Größe und den Schriftstil sind vertauscht: Es müsste new Font("Dialog", Font.BOLD, 12) heißen, denn das erste int steht für den Stil und das zweite int für die Größe. Font.BOLD ist eine int-Konstante.
Konstanten sind nur Namen für Werte eines frei zugänglichen Grundtyps (hier int), und nur die Variablenbelegung, also der Wert, wird an den Konstruktor übergeben. Niemand kann verbieten, dass die Werte direkt eingetragen werden. Das führt dann zu Fehlern wie im oberen Fall, in dem 12 die Ganzzahl für den Schriftstil ist, obwohl es dafür nur die Werte 0, 1, 2 geben sollte. Mit Zeichenketten als Werten der Konstanten kommen wir der Lösung auch nicht näher, wohl aber, wenn der Typ kein int wäre, sondern ein Objekttyp, denn der würde sich vom int unterscheiden.
[»] Hinweis
Ganzzahlen haben aber durchaus ihren Vorteil, wenn es verknüpfte Aufzählungen gibt, also etwa ein hartes und ein raues Material. Das lässt sich durch Materials.HARD + Materials.ROUGH darstellen – was aber nur dann gut funktioniert, wenn jede Konstante ein Bit im Wort einnimmt, wenn also die Werte der Konstanten 1, 2, 4, 8, 16 … lauten.
Eine gute Möglichkeit, von Ganzzahlen wegzukommen, besteht darin, Objekte einer Klasse als Konstanten einzusetzen. Hier muss nicht auf eigene Klassendeklarationen zurückgegriffen werden, sondern Java bietet ein eigenes Sprachmittel.
6.4.3 Aufzählungstypen: typsichere Aufzählungen mit enum
Damit Konstanten von Aufzählungen einen eigenen Typ bekommen und nicht mehr Ganzzahlen oder Strings sind, bietet Java ein Sprachkonstrukt über das Schlüsselwort enum. Die Schreibweise für Aufzählungen erinnert ein wenig an die Deklaration von Klassen und Variablen, nur dass das Schlüsselwort enum statt class gebraucht wird und dass die Variablen automatisch statisch und öffentlich sind und kein Typ angegeben ist. Aufzählungen für Wochentage sind ein gutes Beispiel:
public enum Weekday {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Weekday ist ein Aufzählungstyp. Die Konstantennamen werden wie üblich großgeschrieben – so wie auch statische Variablen, die als Konstanten benutzt werden, großgeschrieben werden.
Aufzählungen nutzen
Um zu verstehen, wie sich Aufzählungstypen nutzen lassen, ist es hilfreich, zu wissen, wie der Compiler sie umsetzt. Intern erstellt der Compiler eine normale Klasse, in unserem Fall Weekday. Alle Aufzählungselemente sind statische Variablen (Konstanten) vom Typ der Aufzählung:
public class Weekday {
public static final Weekday MONDAY = new Weekday( ... );
public static final Weekday TUESDAY = new Weekday( ... );
...
}
Beim Laden der Klasse werden sieben Weekday-Objekte intern angelegt und die statischen Variablen initialisiert. Jetzt ist es einfach, diese Aufzählungen zu nutzen, da sie wie jede andere statische Variable angesprochen werden:
Weekday day = Weekday.SATURDAY;
Hinter den Aufzählungen stehen Objekte, die sich – wie alle anderen – weiterverarbeiten lassen.
if ( day == Weekday.MONDAY )
System.out.println( "'I hate Mondays' (Garfield)" );
Auch implementieren die enum-Konstanten die toString()-Methode, die den Namen der Konstanten liefert.
[»] Geschichte
Sun reservierte zu Beginn der Entwicklung von Java diverse Schlüsselwörter, aber enum war nicht seit Beginn dabei. Als dann in Java 5 plötzlich ein neues Schlüsselwort hinzukam, mussten Entwickler viel Quellcode anpassen und Variablennamen ändern, denn für den Variablentyp java.util.Enumeration war gern der Variablenname enum gewählt worden.
Aufzählungsvergleiche mit ==
Wie die Umsetzung der Aufzählungstypen zeigt, wird für jede Konstante ein Objekt konstruiert, und das sind sogenannte Singletons, also Objekte, die nur einmal erzeugt werden. Eigene neue Aufzählungsobjekte können wir nicht aufbauen, da die Klasse nur einen privaten Konstruktor deklariert. Der Zugriff auf dieses Objekt ist wie ein Zugriff auf eine statische Variable. Der Vergleich zweier Konstanten läuft somit auf den Vergleich von statischen Referenzvariablen hinaus, wofür der Vergleich mit == völlig korrekt ist. Ein equals(…) ist nicht nötig.
[zB] Beispiel
Eine Methode soll entscheiden, ob ein Tag das Wochenende einläutet:
public static boolean isWeekend( Weekday day ) {
return day == Weekday.SATURDAY || day == Weekday.SUNDAY;
}
enum-Konstanten in switch
enum-Konstanten sind in switch-Anweisungen möglich. Das ist möglich, da sie intern über eine Ganzzahl als Identifizierer verfügen, den der Compiler für die Aufzählung einsetzt. Das ist ein ähnliches Konzept, wie es der Compiler auch bei switch auf Strings verfolgt.
Initialisieren wir eine Variable vom Typ Weekday, und nutzen wir eine Fallunterscheidung mit der Aufzählung für einen Test auf das Wochenende:
Weekday day = Weekday.MONDAY;
switch ( day ) {
case SATURDAY: // nicht Weekday.SATURDAY!
case SUNDAY: System.out.println( "Wochenende. Party!" );
}
Dass case Weekday.SATURDAY nicht möglich ist, erklärt sich dadurch, dass mit switch (day) schon der Typ Weekday über die Variable day bestimmt ist. Es ist nicht möglich, dass der Typ der switch-Variablen vom Typ der Variablen in case abweicht.
Referenzen vom Aufzählungstyp können null sein
Dass die Aufzählungen nur Objekte sind, hat eine wichtige Konsequenz. Blicken wir zunächst auf eine Variablendeklaration vom Typ eines enum, die mit einem Wochentag initialisiert ist:
Weekday day = Weekday.MONDAY;
Die Variable day speichert einen Verweis auf das Weekday.MONDAY-Objekt. Das Unschöne an Referenzvariablen ist allerdings, dass sie auch mit null belegt werden können, was so gesehen kein Element der Aufzählung ist:
Weekday day = null;
Wenn solch eine null-Referenz in einem switch landet, gibt es eine NullPointerException, da versteckt im switch ein Zugriff auf die im Enum-Objekt gespeicherte Ordinalzahl stattfindet.
Methoden, die Elemente einer Aufzählung – also Objektverweise – entgegennehmen, sollten im Allgemeinen auf null testen und eine Ausnahme auslösen, um diesen fehlerhaften Teil anzuzeigen; das kann die gegebene Hilfsmethode Objects.requireNonNull(…) übernehmen:
public void setWeekday( Weekday day ) {
this.day = Objects.requireNonNull( day, "Weekday day darf nicht null sein" );
}
Aufzählungstypen als geschachtelten Typ deklarieren *
Es gibt »normale« Aufzählungen, die an normale Klassen erinnern, und auch »geschachtelte« Aufzählungen, die in einen anderen Typ hineingesetzt werden. Mit anderen Worten: Weekday kann auch innerhalb einer anderen Klasse bzw. anderen Schnittstelle deklariert werden. Ist die geschachtelte Aufzählung öffentlich, kann jeder sie nutzen. Sie folgt aber den gleichen Sichtbarkeiten wie Klassen, da Aufzählungen ja nichts anderes als Klassen sind, die der Compiler generiert. Aufzählungen innerhalb von Typen sind immer implizit statisch, das Schlüsselwort static ist also nicht nötig.
[zB] Beispiel
Beide Deklarationen sind identisch, die ohne static ist vorzuziehen:
class Point { enum Color { RED, BLUE, GREEN } } // Üblich
versus
class Point { static enum Color { RED, BLUE, GREEN } } // Unüblich
Statische Importe von Aufzählungen *
Die Aufzählung Weekday hatten wir in das Paket com.tutego.insel.weekday gesetzt. Um auf eine Konstante wie MONDAY zugreifen zu können, wollen wir unterschiedliche import-Varianten nutzen.
Import-Deklaration | Zugriff |
---|---|
import com.tutego.insel.weekday.Weekday; | Weekday.MONDAY |
import com.tutego.insel.weekday.*; | Weekday.MONDAY |
import static com.tutego.insel.weekday.insel. Weekday.*; | MONDAY |
Nehmen wir im Paket als zweites Beispiel eine geschachtelte Aufzählung der Klasse Week hinzu:
package com.tutego.insel.weekday;
public class Week {
public enum Weekday {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
}
Zugriff | |
---|---|
import com.tutego.insel.weekday.Week; | Week.Weekday.MONDAY |
import com.tutego.insel.weekday.Week.Weekday; | Weekday.MONDAY |
import static com.tutego.insel.weekday.Week.Weekday.* | MONDAY |
Standardmethoden der Aufzählungstypen *
Die erzeugten Enum-Objekte bekommen standardmäßig eine Reihe von zusätzlichen Eigenschaften. Wir überschreiben sinnvoll toString(), hashCode() und equals(…) aus Object und implementieren zusätzlich Serializable und Comparable[ 150 ](Die Ordnung der Konstanten ist die Reihenfolge, in der sie geschrieben sind. ), aber nicht Cloneable, da Aufzählungsobjekte nicht geklont werden können. Die Methode toString() liefert den Namen der Konstanten, sodass Weekday.SUNDAY.toString().equals("SUNDAY") wahr ist. Zusätzlich erbt jedes Aufzählungsobjekt von der Spezialklasse Enum, die in Abschnitt 10.7, »Die Spezial-Oberklasse Enum«, näher erklärt wird.