Crear constantes enumeradas en Java

Un conjunto de "constantes enumerables" es una colección ordenada de constantes que se pueden contar, como números. Esa propiedad le permite usarlos como números para indexar una matriz, o puede usarlos como la variable de índice en un bucle for. En Java, estos objetos se conocen con mayor frecuencia como "constantes enumeradas".

El uso de constantes enumeradas puede hacer que el código sea más legible. Por ejemplo, es posible que desee definir un nuevo tipo de datos denominado Color con constantes ROJO, VERDE y AZUL como sus posibles valores. La idea es tener el color como un atributo de otros objetos que cree, como los objetos de automóvil:

class Car {Color color; ...}

Entonces puede escribir código claro y legible, como este:

 myCar.color = ROJO; 

en lugar de algo como:

 myCar.color = 3; 

Un atributo aún más importante de las constantes enumeradas en lenguajes como Pascal es que son seguras para los tipos. En otras palabras, no es posible asignar un color no válido al atributo de color; siempre debe ser ROJO, VERDE o AZUL. Por el contrario, si la variable de color fuera un int, entonces podría asignarle cualquier entero válido, incluso si ese número no representa un color válido.

Este artículo le brinda una plantilla para crear constantes enumeradas que son:

  • Escriba seguro
  • Imprimible
  • Ordenado, para usar como índice
  • Vinculado, para avanzar o retroceder
  • Enumerable

En un artículo futuro, aprenderá cómo extender las constantes enumeradas para implementar el comportamiento dependiente del estado.

¿Por qué no utilizar finales estáticas?

Un mecanismo común para las constantes enumeradas utiliza variables int finales estáticas, como esta:

estático final int RED = 0; estático final int VERDE = 1; estático final int AZUL = 2; ...

Las finales estáticas son útiles

Como son finales, los valores son constantes e inmutables. Debido a que son estáticos, solo se crean una vez para la clase o interfaz en la que están definidos, en lugar de una vez para cada objeto. Y debido a que son variables enteras, se pueden enumerar y usar como índice.

Por ejemplo, puede escribir un bucle para crear una lista de los colores favoritos de un cliente:

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

También puede indexar en una matriz o un vector utilizando las variables para obtener un valor asociado con el color. Por ejemplo, suponga que tiene un juego de mesa que tiene piezas de diferentes colores para cada jugador. Digamos que tiene un mapa de bits para cada pieza de color y un método llamado display()que copia ese mapa de bits en la ubicación actual. Una forma de poner una pieza en el tablero podría ser algo como esto:

PiecePicture redPiece = nueva PiecePicture (RED); PiecePicture greenPiece = nueva PiecePicture (VERDE); PiecePicture bluePiece = nueva PiecePicture (AZUL);

void placePiece (int ubicación, int color) {setPosition (ubicación); if (color == RED) {display (redPiece); } else if (color == VERDE) {display (greenPiece); } else {display (bluePiece); }}

Pero al usar los valores enteros para indexar en una matriz de piezas, puede simplificar el código para:

PiecePicture [] piece = {nueva PiecePicture (ROJO), nueva PiecePicture (VERDE), nueva PiecePicture (AZUL)}; void placePiece (int ubicación, int color) {setPosition (ubicación); display (pieza [color]); }

Ser capaz de recorrer un rango de constantes e indexar en una matriz o vector son las principales ventajas de los enteros finales estáticos. Y cuando aumenta el número de opciones, el efecto de simplificación es aún mayor.

Pero las finales estáticas son arriesgadas

Aún así, hay un par de inconvenientes al usar enteros finales estáticos. El mayor inconveniente es la falta de seguridad de tipos. Cualquier entero que se calcule o lea se puede usar como un "color", independientemente de si tiene sentido hacerlo. Puede recorrer el final de las constantes definidas o dejar de cubrirlas todas, lo que puede suceder fácilmente si agrega o elimina una constante de la lista, pero se olvida de ajustar el índice del ciclo.

Por ejemplo, su ciclo de preferencia de color podría leerse así:

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

Más adelante, puede agregar un nuevo color:

estático final int RED = 0; estático final int VERDE = 1; estático final int AZUL = 2; estático final int MAGENTA = 3;

O puede eliminar uno:

estático final int RED = 0; estático final int AZUL = 1;

En cualquier caso, el programa no funcionará correctamente. Si elimina un color, obtendrá un error de tiempo de ejecución que llamará la atención sobre el problema. Si agrega un color, no obtendrá ningún error: el programa simplemente no cubrirá todas las opciones de color.

Otro inconveniente es la falta de un identificador legible. Si usa un cuadro de mensaje o una salida de consola para mostrar la elección de color actual, obtendrá un número. Eso hace que la depuración sea bastante difícil.

Los problemas para crear un identificador legible a veces se resuelven utilizando constantes de cadena final estáticas, como esta:

Cadena final estática RED = "rojo" .intern (); ...

El uso del intern()método garantiza que solo haya una cadena con esos contenidos en el grupo de cadenas interno. Pero para intern()que sea eficaz, cada cadena o variable de cadena que se compare con RED debe usarla. Incluso entonces, las cadenas finales estáticas no permiten el bucle o la indexación en una matriz, y aún no abordan el problema de la seguridad de tipos.

Tipo de seguridad

El problema con los enteros finales estáticos es que las variables que los usan son inherentemente ilimitadas. Son variables int, lo que significa que pueden contener cualquier número entero, no solo las constantes que debían contener. El objetivo es definir una variable de tipo Color para que obtenga un error de compilación en lugar de un error de tiempo de ejecución siempre que se asigne un valor no válido a esa variable.

Se proporcionó una solución elegante en el artículo de Philip Bishop en JavaWorld, "Constantes de seguridad de tipos en C ++ y Java".

La idea es realmente simple (¡una vez que la veas!):

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

public static final Color ROJO = nuevo Color (); public static final Color VERDE = nuevo Color (); público estático final Color AZUL = nuevo 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

El siguiente paso es poder iterar sobre las constantes de clase. Desea poder realizar un bucle de principio a fin:

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

o desde el final hasta el principio:

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

Estas modificaciones utilizan variables estáticas para realizar un seguimiento del último objeto creado y vincularlo al siguiente objeto: