Una guía de enumeraciones de Java

1. Información general

En este artículo, veremos qué son las enumeraciones de Java, qué problemas resuelven y cómo algunos de los patrones de diseño se pueden utilizar en la práctica.

La palabra clave enum se introdujo en Java 5. Denota un tipo especial de clase que siempre extiende la clase java.lang.Enum . Para obtener la documentación oficial sobre su uso, consulte la documentación.

Las constantes definidas de esta manera hacen que el código sea más legible, permiten la verificación en tiempo de compilación, documentan por adelantado la lista de valores aceptados y evitan comportamientos inesperados debido a la transmisión de valores no válidos.

Aquí hay un ejemplo rápido y sencillo de una enumeración que define el estado de un pedido de pizza; el estado del pedido puede ser PEDIDO , LISTO o ENTREGADO :

public enum PizzaStatus { ORDERED, READY, DELIVERED; }

Además, vienen con muchos métodos útiles, que de otro modo tendría que escribir usted mismo si estuviera usando constantes finales estáticas públicas tradicionales.

2. Métodos de enumeración personalizados

Bien, ahora que tenemos una comprensión básica de qué son las enumeraciones y cómo puede usarlas, llevemos nuestro ejemplo anterior al siguiente nivel definiendo algunos métodos API adicionales en la enumeración:

public class Pizza { private PizzaStatus status; public enum PizzaStatus { ORDERED, READY, DELIVERED; } public boolean isDeliverable() { if (getStatus() == PizzaStatus.READY) { return true; } return false; } // Methods that set and get the status variable. } 

3. Comparación de tipos de enumeración con el operador "=="

Dado que los tipos de enumeración garantizan que solo exista una instancia de las constantes en la JVM, podemos usar con seguridad el operador “==” para comparar dos variables como se ve en el ejemplo anterior; además, el operador "==" proporciona seguridad en tiempo de compilación y tiempo de ejecución.

Primero echemos un vistazo a la seguridad en tiempo de ejecución en el siguiente fragmento donde el operador "==" se usa para comparar estados y no se lanzará una NullPointerException si alguno de los valores es nulo . Por el contrario , se lanzaría una NullPointerException si se usara el método equals:

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); 

En cuanto a la seguridad del tiempo de compilación , echemos un vistazo a otro ejemplo en el que se compara una enumeración de un tipo diferente utilizando el método equals , porque se determina que los valores de la enumeración y el método getStatus son coincidentemente iguales, pero lógicamente el la comparación debería ser falsa. Este problema se evita utilizando el operador "==".

El compilador marcará la comparación como un error de incompatibilidad:

if(testPz.getStatus().equals(TestColor.GREEN)); if(testPz.getStatus() == TestColor.GREEN); 

4. Uso de tipos de enumeración en declaraciones Switch

Los tipos de enumeración también se pueden usar en declaraciones de cambio :

public int getDeliveryTimeInDays() { switch (status) { case ORDERED: return 5; case READY: return 2; case DELIVERED: return 0; } return 0; }

5. Campos, métodos y constructores en enumeraciones

Puede definir constructores, métodos y campos dentro de los tipos de enumeración que lo hacen muy poderoso.

Extendamos el ejemplo anterior e implementemos la transición de una etapa de una pizza a otra y veamos cómo podemos deshacernos de la declaración if y la declaración switch utilizada antes:

public class Pizza { private PizzaStatus status; public enum PizzaStatus { ORDERED (5){ @Override public boolean isOrdered() { return true; } }, READY (2){ @Override public boolean isReady() { return true; } }, DELIVERED (0){ @Override public boolean isDelivered() { return true; } }; private int timeToDelivery; public boolean isOrdered() {return false;} public boolean isReady() {return false;} public boolean isDelivered(){return false;} public int getTimeToDelivery() { return timeToDelivery; } PizzaStatus (int timeToDelivery) { this.timeToDelivery = timeToDelivery; } } public boolean isDeliverable() { return this.status.isReady(); } public void printTimeToDeliver() { System.out.println("Time to delivery is " + this.getStatus().getTimeToDelivery()); } // Methods that set and get the status variable. } 

El fragmento de prueba a continuación demuestra cómo funciona esto:

@Test public void givenPizaOrder_whenReady_thenDeliverable() { Pizza testPz = new Pizza(); testPz.setStatus(Pizza.PizzaStatus.READY); assertTrue(testPz.isDeliverable()); }

6. EnumSet y EnumMap

6.1. EnumSet

El EnumSet es un especializado Conjunto aplicación destinado a ser usado con Enum tipos.

Es una representación muy eficiente y compacta de un conjunto particular de constantes Enum en comparación con un HashSet , debido a la representación de vector de bits interna que se utiliza. Y proporciona una alternativa segura de tipos a los tradicionales "indicadores de bits" basados ​​en int , lo que nos permite escribir código conciso que es más legible y fácil de mantener.

El EnumSet es una clase abstracta que tiene dos implementaciones llamados RegularEnumSet y JumboEnumSet , uno de los cuales se elige en función del número de constantes en la enumeración en el momento de creación de instancias.

Por lo tanto, siempre es una buena idea usar este conjunto siempre que queramos trabajar con una colección de constantes de enumeración en la mayoría de los escenarios (como subconjuntos, agregar, eliminar y para operaciones masivas como containsAll y removeAll ) y usar Enum.values ​​( ) si solo desea iterar sobre todas las constantes posibles.

En el fragmento de código a continuación, puede ver cómo se usa EnumSet para crear un subconjunto de constantes y su uso:

public class Pizza { private static EnumSet undeliveredPizzaStatuses = EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY); private PizzaStatus status; public enum PizzaStatus { ... } public boolean isDeliverable() { return this.status.isReady(); } public void printTimeToDeliver() { System.out.println("Time to delivery is " + this.getStatus().getTimeToDelivery() + " days"); } public static List getAllUndeliveredPizzas(List input) { return input.stream().filter( (s) -> undeliveredPizzaStatuses.contains(s.getStatus())) .collect(Collectors.toList()); } public void deliver() { if (isDeliverable()) { PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() .deliver(this); this.setStatus(PizzaStatus.DELIVERED); } } // Methods that set and get the status variable. } 

La ejecución de la siguiente prueba demostró el poder de la implementación EnumSet de la interfaz Set :

@Test public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { List pzList = new ArrayList(); Pizza pz1 = new Pizza(); pz1.setStatus(Pizza.PizzaStatus.DELIVERED); Pizza pz2 = new Pizza(); pz2.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz3 = new Pizza(); pz3.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz4 = new Pizza(); pz4.setStatus(Pizza.PizzaStatus.READY); pzList.add(pz1); pzList.add(pz2); pzList.add(pz3); pzList.add(pz4); List undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); assertTrue(undeliveredPzs.size() == 3); }

6.2. EnumMap

EnumMap es una implementación de Map especializada diseñada para usarse con constantes enum como claves. Es una implementación eficiente y compacta en comparación con su contraparte HashMap y se representa internamente como una matriz:

EnumMap map; 

Echemos un vistazo rápido a un ejemplo real que muestra cómo se puede usar en la práctica:

public static EnumMap
    
      groupPizzaByStatus(List pizzaList) { EnumMap
     
       pzByStatus = new EnumMap
      
       (PizzaStatus.class); for (Pizza pz : pizzaList) { PizzaStatus status = pz.getStatus(); if (pzByStatus.containsKey(status)) { pzByStatus.get(status).add(pz); } else { List newPzList = new ArrayList(); newPzList.add(pz); pzByStatus.put(status, newPzList); } } return pzByStatus; } 
      
     
    

La ejecución de la siguiente prueba demostró el poder de la implementación de EnumMap de la interfaz Map :

@Test public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { List pzList = new ArrayList(); Pizza pz1 = new Pizza(); pz1.setStatus(Pizza.PizzaStatus.DELIVERED); Pizza pz2 = new Pizza(); pz2.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz3 = new Pizza(); pz3.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz4 = new Pizza(); pz4.setStatus(Pizza.PizzaStatus.READY); pzList.add(pz1); pzList.add(pz2); pzList.add(pz3); pzList.add(pz4); EnumMap
    
      map = Pizza.groupPizzaByStatus(pzList); assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1); assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2); assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1); }
    

7. Implementar patrones de diseño mediante enumeraciones

7.1. Patrón Singleton

Normalmente, implementar una clase utilizando el patrón Singleton no es nada trivial. Las enumeraciones proporcionan una forma fácil y rápida de implementar singletons.

In addition to that, since the enum class implements the Serializable interface under the hood, the class is guaranteed to be a singleton by the JVM, which unlike the conventional implementation where we have to ensure that no new instances are created during deserialization.

In the code snippet below, we see how we can implement singleton pattern:

public enum PizzaDeliverySystemConfiguration { INSTANCE; PizzaDeliverySystemConfiguration() { // Initialization configuration which involves // overriding defaults like delivery strategy } private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL; public static PizzaDeliverySystemConfiguration getInstance() { return INSTANCE; } public PizzaDeliveryStrategy getDeliveryStrategy() { return deliveryStrategy; } }

7.2. Strategy Pattern

Conventionally the Strategy pattern is written by having an interface that is implemented by different classes.

Adding a new strategy meant adding a new implementation class. With enums, this is achieved with less effort, adding a new implementation means defining just another instance with some implementation.

The code snippet below shows how to implement the Strategy pattern:

public enum PizzaDeliveryStrategy { EXPRESS { @Override public void deliver(Pizza pz) { System.out.println("Pizza will be delivered in express mode"); } }, NORMAL { @Override public void deliver(Pizza pz) { System.out.println("Pizza will be delivered in normal mode"); } }; public abstract void deliver(Pizza pz); }

Add the following method to the Pizza class:

public void deliver() { if (isDeliverable()) { PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() .deliver(this); this.setStatus(PizzaStatus.DELIVERED); } }
@Test public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() { Pizza pz = new Pizza(); pz.setStatus(Pizza.PizzaStatus.READY); pz.deliver(); assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED); }

8. Java 8 and Enums

The Pizza class can be rewritten in Java 8, and you can see how the methods getAllUndeliveredPizzas() and groupPizzaByStatus() become so concise with the use of lambdas and the Stream APIs:

public static List getAllUndeliveredPizzas(List input) { return input.stream().filter( (s) -> !deliveredPizzaStatuses.contains(s.getStatus())) .collect(Collectors.toList()); } 
public static EnumMap
    
      groupPizzaByStatus(List pzList) { EnumMap
     
       map = pzList.stream().collect( Collectors.groupingBy(Pizza::getStatus, () -> new EnumMap(PizzaStatus.class), Collectors.toList())); return map; }
     
    

9. JSON Representation of Enum

Using Jackson libraries, it is possible to have a JSON representation of enum types as if they are POJOs. The code snippet below shows the Jackson annotations that can be used for the same:

@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum PizzaStatus { ORDERED (5){ @Override public boolean isOrdered() { return true; } }, READY (2){ @Override public boolean isReady() { return true; } }, DELIVERED (0){ @Override public boolean isDelivered() { return true; } }; private int timeToDelivery; public boolean isOrdered() {return false;} public boolean isReady() {return false;} public boolean isDelivered(){return false;} @JsonProperty("timeToDelivery") public int getTimeToDelivery() { return timeToDelivery; } private PizzaStatus (int timeToDelivery) { this.timeToDelivery = timeToDelivery; } } 

We can use the Pizza and PizzaStatus as follows:

Pizza pz = new Pizza(); pz.setStatus(Pizza.PizzaStatus.READY); System.out.println(Pizza.getJsonString(pz)); 

to generate the following JSON representation of the Pizzas status:

{ "status" : { "timeToDelivery" : 2, "ready" : true, "ordered" : false, "delivered" : false }, "deliverable" : true }

For more information on JSON serializing/deserializing (including customization) of enum types refer to the Jackson – Serialize Enums as JSON Objects.

10. Conclusion

En este artículo, exploramos la enumeración de Java, desde los conceptos básicos del lenguaje hasta casos de uso del mundo real más avanzados e interesantes.

Los fragmentos de código de este artículo se pueden encontrar en el repositorio de Github.