Créer des constantes énumérées en Java

Un ensemble de «constantes énumérables» est une collection ordonnée de constantes qui peuvent être comptées, comme des nombres. Cette propriété vous permet de les utiliser comme des nombres pour indexer un tableau, ou vous pouvez les utiliser comme variable d'index dans une boucle for. En Java, ces objets sont le plus souvent appelés «constantes énumérées».

L'utilisation de constantes énumérées peut rendre le code plus lisible. Par exemple, vous pouvez définir un nouveau type de données nommé Couleur avec les constantes RED, GREEN et BLUE comme valeurs possibles. L'idée est d'avoir Color comme attribut des autres objets que vous créez, tels que les objets Car:

class Car {Couleur couleur; ...}

Ensuite, vous pouvez écrire un code clair et lisible, comme ceci:

 myCar.color = ROUGE; 

au lieu de quelque chose comme:

 myCar.color = 3; 

Un attribut encore plus important des constantes énumérées dans des langages comme Pascal est qu'elles sont de type sécurisé. En d'autres termes, il n'est pas possible d'attribuer une couleur non valide à l'attribut de couleur - il doit toujours être ROUGE, VERT ou BLEU. En revanche, si la variable de couleur était un entier, vous pouvez lui attribuer n'importe quel entier valide, même si ce nombre ne représente pas une couleur valide.

Cet article vous propose un modèle pour créer des constantes énumérées qui sont:

  • Type coffre-fort
  • Imprimable
  • Commandé, pour une utilisation comme index
  • Lié, pour boucler en avant ou en arrière
  • Énumérable

Dans un prochain article, vous apprendrez comment étendre les constantes énumérées pour implémenter un comportement dépendant de l'état.

Pourquoi ne pas utiliser des finales statiques?

Un mécanisme courant pour les constantes énumérées utilise des variables int finales statiques, comme ceci:

statique final int RED = 0; statique final int VERT = 1; statique final int BLEU = 2; ...

Les finales statiques sont utiles

Parce qu'elles sont définitives, les valeurs sont constantes et immuables. Parce qu'ils sont statiques, ils ne sont créés qu'une seule fois pour la classe ou l'interface dans laquelle ils sont définis, au lieu d'une fois pour chaque objet. Et comme ce sont des variables entières, elles peuvent être énumérées et utilisées comme index.

Par exemple, vous pouvez écrire une boucle pour créer une liste des couleurs préférées d'un client:

for (int i = 0; ...) {if (customerLikesColor (i)) {favoriteColors.add (i); }}

Vous pouvez également indexer dans un tableau ou un vecteur en utilisant les variables pour obtenir une valeur associée à la couleur. Par exemple, supposons que vous ayez un jeu de société qui a des pièces de couleurs différentes pour chaque joueur. Disons que vous avez une image bitmap pour chaque pièce de couleur et une méthode appelée display()qui copie cette image bitmap à l'emplacement actuel. Une façon de mettre un morceau sur le tableau pourrait être quelque chose comme ceci:

PiecePicture redPiece = nouvelle PiecePicture (RED); PiecePicture greenPiece = nouvelle PiecePicture (VERT); PiecePicture bluePiece = nouvelle PiecePicture (BLEU);

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

Mais en utilisant les valeurs entières pour indexer dans un tableau de pièces, vous pouvez simplifier le code pour:

PiecePicture [] piece = {new PiecePicture (RED), new PiecePicture (GREEN), new PiecePicture (BLUE)}; void placePiece (int location, int color) {setPosition (location); affichage (pièce [couleur]); }

Être capable de boucler sur une plage de constantes et d'indexer dans un tableau ou un vecteur sont les principaux avantages des entiers finaux statiques. Et lorsque le nombre de choix augmente, l'effet de simplification est encore plus grand.

Mais les finales statiques sont risquées

Pourtant, il y a quelques inconvénients à utiliser des entiers finaux statiques. L'inconvénient majeur est le manque de sécurité du type. Tout entier calculé ou lu peut être utilisé comme «couleur», qu'il soit logique ou non de le faire. Vous pouvez boucler juste au-delà de la fin des constantes définies ou arrêter de les couvrir toutes, ce qui peut facilement se produire si vous ajoutez ou supprimez une constante de la liste mais oubliez d'ajuster l'index de boucle.

Par exemple, votre boucle de préférence de couleur peut se lire comme suit:

for (int i = 0; i <= BLUE; i ++) {if (customerLikesColor (i)) {favoriteColors.add (i); }}

Plus tard, vous pourrez ajouter une nouvelle couleur:

statique final int RED = 0; statique final int VERT = 1; statique final int BLEU = 2; statique final int MAGENTA = 3;

Ou vous pouvez en supprimer un:

statique final int RED = 0; statique final int BLEU = 1;

Dans les deux cas, le programme ne fonctionnera pas correctement. Si vous supprimez une couleur, vous obtiendrez une erreur d'exécution qui attire l'attention sur le problème. Si vous ajoutez une couleur, vous n'obtiendrez aucune erreur - le programme échouera tout simplement à couvrir tous les choix de couleurs.

Un autre inconvénient est l'absence d'identifiant lisible. Si vous utilisez une boîte de message ou une sortie de console pour afficher le choix de couleur actuel, vous obtenez un nombre. Cela rend le débogage assez difficile.

Les problèmes de création d'un identifiant lisible sont parfois résolus à l'aide de constantes de chaîne finales statiques, comme ceci:

statique final String RED = "rouge" .intern (); ...

L'utilisation de la intern()méthode garantit qu'il n'y a qu'une seule chaîne avec ce contenu dans le pool de chaînes interne. Mais pour intern()être efficace, chaque chaîne ou variable de chaîne qui est jamais comparée à RED doit l'utiliser. Même dans ce cas, les chaînes finales statiques ne permettent pas la mise en boucle ou l'indexation dans un tableau, et elles ne résolvent toujours pas le problème de la sécurité de type.

Sécurité de type

Le problème avec les entiers finaux statiques est que les variables qui les utilisent sont intrinsèquement illimitées. Ce sont des variables int, ce qui signifie qu'elles peuvent contenir n'importe quel entier, pas seulement les constantes qu'elles étaient censées contenir. Le but est de définir une variable de type Color afin d'obtenir une erreur de compilation plutôt qu'une erreur d'exécution chaque fois qu'une valeur non valide est affectée à cette variable.

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

L'étape suivante consiste à pouvoir parcourir les constantes de classe. Vous voulez pouvoir boucler du début à la fin:

 pour (Color c = Color.first (); c! = null; c = c.next ()) {...} 

ou de la fin au début:

 pour (Color c = Color.last (); c! = null; c = c.prev ()) {...} 

Ces modifications utilisent des variables statiques pour garder une trace du dernier objet créé et le lier à l'objet suivant: