Erstellen Sie aufgezählte Konstanten in Java

Eine Menge von "aufzählbaren Konstanten" ist eine geordnete Sammlung von Konstanten, die wie Zahlen gezählt werden können. Mit dieser Eigenschaft können Sie sie wie Zahlen verwenden, um ein Array zu indizieren, oder Sie können sie als Indexvariable in einer for-Schleife verwenden. In Java werden solche Objekte am häufigsten als "aufgezählte Konstanten" bezeichnet.

Die Verwendung von Aufzählungskonstanten kann die Lesbarkeit von Code verbessern. Beispielsweise möchten Sie möglicherweise einen neuen Datentyp mit dem Namen Farbe definieren, dessen mögliche Werte die Konstanten ROT, GRÜN und BLAU sind. Die Idee ist, Farbe als Attribut für andere Objekte zu verwenden, die Sie erstellen, z. B. Autoobjekte:

Klasse Auto {Farbe Farbe; ...}

Dann können Sie klaren, lesbaren Code wie folgt schreiben:

 myCar.color = RED; 

statt so etwas wie:

 myCar.color = 3; 

Ein noch wichtigeres Merkmal von aufgezählten Konstanten in Sprachen wie Pascal ist, dass sie typsicher sind. Mit anderen Worten, es ist nicht möglich, dem Farbattribut eine ungültige Farbe zuzuweisen - es muss immer entweder ROT, GRÜN oder BLAU sein. Wenn die Farbvariable dagegen ein int wäre, könnten Sie ihr eine gültige Ganzzahl zuweisen, selbst wenn diese Zahl keine gültige Farbe darstellt.

Dieser Artikel enthält eine Vorlage zum Erstellen von Aufzählungskonstanten:

  • Geben Sie safe ein
  • Druckbar
  • Bestellt, zur Verwendung als Index
  • Verknüpft, um vorwärts oder rückwärts zu schleifen
  • Aufzählbar

In einem zukünftigen Artikel erfahren Sie, wie Sie aufgezählte Konstanten erweitern, um zustandsabhängiges Verhalten zu implementieren.

Warum nicht statische Finals verwenden?

Ein allgemeiner Mechanismus für aufgezählte Konstanten verwendet statische endgültige int-Variablen wie folgt:

statisch final int ROT = 0; statisches Finale int GRÜN = 1; statisch final int BLAU = 2; ...

Statische Endspiele sind nützlich

Da sie endgültig sind, sind die Werte konstant und unveränderlich. Da sie statisch sind, werden sie nur einmal für die Klasse oder Schnittstelle erstellt, in der sie definiert sind, und nicht einmal für jedes Objekt. Und da es sich um ganzzahlige Variablen handelt, können sie aufgelistet und als Index verwendet werden.

Sie können beispielsweise eine Schleife schreiben, um eine Liste der Lieblingsfarben eines Kunden zu erstellen:

für (int i = 0; ...) {if (customerLikesColor (i)) {FavoriteColors.add (i); }}

Sie können mithilfe der Variablen auch in ein Array oder einen Vektor indizieren, um einen der Farbe zugeordneten Wert zu erhalten. Angenommen, Sie haben ein Brettspiel, das für jeden Spieler verschiedenfarbige Teile enthält. Angenommen, Sie haben eine Bitmap für jedes Farbstück und eine Methode namens display(), die diese Bitmap an den aktuellen Speicherort kopiert. Eine Möglichkeit, ein Stück auf die Tafel zu legen, könnte etwa so aussehen:

PiecePicture redPiece = neues PiecePicture (ROT); PiecePicture greenPiece = neues PiecePicture (GRÜN); PiecePicture bluePiece = neues PiecePicture (BLAU);

void placePiece (int location, int color) {setPosition (location); if (color == RED) {display (redPiece); } else if (color == GREEN) {display (greenPiece); } else {display (bluePiece); }}

Indem Sie die ganzzahligen Werte zum Indizieren in ein Array von Teilen verwenden, können Sie den Code vereinfachen, um:

PiecePicture []piece = {neues PiecePicture (ROT), neues PiecePicture (GRÜN), neues PiecePicture (BLAU)}; void placePiece (int location, int color) {setPosition (location); Anzeige (Stück [Farbe]); }}

Die Möglichkeit, einen Bereich von Konstanten zu durchlaufen und in ein Array oder einen Vektor zu indizieren, sind die Hauptvorteile statischer endgültiger Ganzzahlen. Und wenn die Anzahl der Auswahlmöglichkeiten zunimmt, ist der Vereinfachungseffekt noch größer.

Aber statische Endspiele sind riskant

Die Verwendung statischer endgültiger Ganzzahlen weist jedoch einige Nachteile auf. Der Hauptnachteil ist die mangelnde Typensicherheit. Jede berechnete oder eingelesene Ganzzahl kann als "Farbe" verwendet werden, unabhängig davon, ob dies sinnvoll ist. Sie können direkt nach dem Ende der definierten Konstanten eine Schleife erstellen oder alle nicht mehr abdecken. Dies kann leicht passieren, wenn Sie eine Konstante zur Liste hinzufügen oder daraus entfernen, aber vergessen, den Schleifenindex anzupassen.

Beispielsweise könnte Ihre Farbpräferenzschleife folgendermaßen lauten:

für (int i = 0; i <= BLAU; i ++) {if (customerLikesColor (i)) {FavoriteColors.add (i); }}

Später können Sie eine neue Farbe hinzufügen:

statisch final int ROT = 0; statisches Finale int GRÜN = 1; statisch final int BLAU = 2; statisches Finale int MAGENTA = 3;

Oder Sie entfernen eine:

statisch final int ROT = 0; statisch final int BLAU = 1;

In beiden Fällen funktioniert das Programm nicht richtig. Wenn Sie eine Farbe entfernen, wird ein Laufzeitfehler angezeigt, der auf das Problem aufmerksam macht. Wenn Sie eine Farbe hinzufügen, wird überhaupt kein Fehler angezeigt - das Programm deckt einfach nicht alle Farboptionen ab.

Ein weiterer Nachteil ist das Fehlen einer lesbaren Kennung. Wenn Sie ein Meldungsfeld oder eine Konsolenausgabe verwenden, um die aktuelle Farbauswahl anzuzeigen, erhalten Sie eine Nummer. Das macht das Debuggen ziemlich schwierig.

Die Probleme beim Erstellen eines lesbaren Bezeichners werden manchmal mithilfe statischer endgültiger Zeichenfolgenkonstanten wie folgt gelöst:

statische letzte Zeichenfolge ROT = "rot" .intern (); ...

Die Verwendung der intern()Methode garantiert, dass nur eine Zeichenfolge mit diesen Inhalten im internen Zeichenfolgenpool vorhanden ist. Um intern()jedoch effektiv zu sein, muss jede Zeichenfolge oder Zeichenfolgenvariable, die jemals mit ROT verglichen wird, diese verwenden. Selbst dann erlauben statische endgültige Zeichenfolgen keine Schleife oder Indizierung in ein Array, und sie behandeln das Problem der Typensicherheit immer noch nicht.

Typensicherheit

Das Problem mit statischen endgültigen Ganzzahlen besteht darin, dass die Variablen, die sie verwenden, von Natur aus unbegrenzt sind. Sie sind int-Variablen, dh sie können eine beliebige Ganzzahl enthalten, nicht nur die Konstanten, die sie enthalten sollen. Ziel ist es, eine Variable vom Typ Color zu definieren, sodass Sie einen Kompilierungsfehler anstelle eines Laufzeitfehlers erhalten, wenn dieser Variablen ein ungültiger Wert zugewiesen wird.

An elegant solution was provided in Philip Bishop's article in JavaWorld, "Typesafe constants in C++ and Java."

The idea is really simple (once you see it!):

public final class Color { // final class!! private Color() {} // private constructor!!

public static final Color RED = new Color(); public static final Color GREEN = new Color(); public static final Color BLUE = new Color(); }

Because the class is defined as final, it can't be subclassed. No other classes will be created from it. Because the constructor is private, other methods can't use the class to create new objects. The only objects that will ever be created with this class are the static objects the class creates for itself the first time the class is referenced! This implementation is a variation of the Singleton pattern that limits the class to a predefined number of instances. You can use this pattern to create exactly one class any time you need a Singleton, or use it as shown here to create a fixed number of instances. (The Singleton pattern is defined in the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, Addison-Wesley, 1995. See the Resources section for a link to this book.)

The mind-boggling part of this class definition is that the class uses itself to create new objects. The first time you reference RED, it doesn't exist. But the act of accessing the class that RED is defined in causes it to be created, along with the other constants. Admittedly, that kind of recursive reference is rather difficult to visualize. But the advantage is total type safety. A variable of type Color can never be assigned anything other than the RED, GREEN, or BLUE objects that the Color class creates.

Identifiers

The first enhancement to the typesafe enumerated constant class is to create a string representation of the constants. You want to be able to produce a readable version of the value with a line like this:

 System.out.println(myColor); 

Whenever you output an object to a character output stream like System.out, and whenever you concatenate an object to a string, Java automatically invokes the toString() method for that object. That's a good reason to define a toString() method for any new class you create.

If the class does not have a toString() method, the inheritance hierarchy is inspected until one is found. At the top of the hierarchy, the toString() method in the Object class returns the class name. So the toString() method always has some meaning, but most of the time the default method will not be very useful.

Here is a modification to the Color class that provides a useful toString() method:

public final class Color { private String id; private Color(String anID) {this.id = anID; } public String toString() {return this.id; }

public static final Color RED = new Color(

"Red"

); public static final Color GREEN = new Color(

"Green"

); public static final Color BLUE = new Color(

"Blue"

); }

This version adds a private String variable (id). The constructor has been modified to take a String argument and store it as the object's ID. The toString() method then returns the object's ID.

One trick you can use to invoke the toString() method takes advantage of the fact that it is automatically invoked when an object is concatenated to a string. That means you could put the object's name in a dialog by concatenating it to a null string using a line like the following:

 textField1.setText("" + myColor); 

Unless you happen to love all the parentheses in Lisp, you will find that a bit more readable than the alternative:

 textField1.setText(myColor.toString()); 

It's also easier to make sure you put in the right number of closing parentheses!

Ordering and indexing

The next question is how to index into a vector or an array using members of the

Color

class. The mechanism will be to assign an ordinal number to each class constant and reference it using the attribute

.ord

, like this:

 void placePiece(int location, int color) { setPosition(location); display(piece[color.ord]); } 

Although tacking on .ord to convert the reference to color into a number is not particularly pretty, it is not terribly obtrusive either. It seems like a fairly reasonable tradeoff for typesafe constants.

Here is how the ordinal numbers are assigned:

public final class Color { private String id; public final int ord;private static int upperBound = 0; private Color(String anID) { this.id = anID; this.ord = upperBound++; } public String toString() {return this.id; } public static int size() { return upperBound; }

public static final Color RED = new Color("Red"); public static final Color GREEN = new Color("Green"); public static final Color BLUE = new Color("Blue"); }

This code uses the new JDK version 1.1 definition of a "blank final" variable -- a variable that is assigned a value once and once only. This mechanism allows each object to have its own non-static final variable, ord, which will be assigned once during object creation and which will thereafter remain immutable. The static variable upperBound keeps track of the next unused index in the collection. That value becomes the ord attribute when the object is created, after which the upper bound is incremented.

For compatibility with the Vector class, the method size() is defined to return the number of constants that have been defined in this class (which is the same as the upper bound).

A purist might decide that the variable ord should be private, and the method named ord() should return it -- if not, a method named getOrd(). I lean toward accessing the attribute directly, though, for two reasons. The first is that the concept of an ordinal is unequivocally that of an int. There is little likelihood, if any, that the implementation would ever change. The second reason is that what you really want is the ability to use the object as though it were an int, as you could in a language like Pascal. For example, you might want to use the attribute color to index an array. But you cannot use a Java object to do that directly. What you would really like to say is:

 display(piece[color]); // desirable, but does not work 

But you can't do that. The minimum change necessary to get what you want is to access an attribute, instead, like this:

 display(piece[color.ord]); // closest to desirable 

instead of the lengthy alternative:

 display(piece[color.ord()]); // extra parentheses 

or the even lengthier:

 display(piece[color.getOrd()]); // extra parentheses and text 

The Eiffel language uses the same syntax for accessing attributes and invoking methods. That would be the ideal. Given the necessity of choosing one or the other, however, I've gone with accessing ord as an attribute. With any luck, the identifier ord will become so familiar as a result of repetition that using it will seem as natural as writing int. (As natural as that may be.)

Looping

Der nächste Schritt besteht darin, die Klassenkonstanten durchlaufen zu können. Sie möchten in der Lage sein, eine Schleife von Anfang bis Ende durchzuführen:

 für (Farbe c = Color.first (); c! = null; c = c.next ()) {...} 

oder vom Ende zurück zum Anfang:

 für (Farbe c = Color.last (); c! = null; c = c.prev ()) {...} 

Diese Änderungen verwenden statische Variablen, um das zuletzt erstellte Objekt zu verfolgen und es mit dem nächsten Objekt zu verknüpfen: