Una guida alle enumerazioni Java

1. Panoramica

In questo articolo, vedremo cosa sono le enumerazioni Java, quali problemi risolvono e come alcuni dei design pattern possono essere utilizzati nella pratica.

La parola chiave enum è stata introdotta in Java 5. Denota un tipo speciale di classe che estende sempre la classe java.lang.Enum . Per la documentazione ufficiale sul loro utilizzo dai un'occhiata alla documentazione.

Le costanti definite in questo modo rendono il codice più leggibile, consentono il controllo in fase di compilazione, documentano in anticipo l'elenco dei valori accettati ed evitano comportamenti imprevisti dovuti al passaggio di valori non validi.

Ecco un rapido e semplice esempio di enumerazione che definisce lo stato di un ordine per una pizza; lo stato dell'ordine può essere ORDINATO , PRONTO o CONSEGNATO :

public enum PizzaStatus { ORDERED, READY, DELIVERED; }

Inoltre, vengono forniti con molti metodi utili, che altrimenti dovresti scrivere da solo se stessi usando le costanti finali statiche pubbliche tradizionali.

2. Metodi Enum personalizzati

OK, quindi ora che abbiamo una conoscenza di base di cosa sono le enumerazioni e come puoi usarle, portiamo il nostro esempio precedente al livello successivo definendo alcuni metodi API extra sull'enumerazione:

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. Confronto dei tipi di enum utilizzando l'operatore "=="

Poiché i tipi enum assicurano che una sola istanza delle costanti esista nella JVM, possiamo tranquillamente usare l'operatore "==" per confrontare due variabili come mostrato nell'esempio sopra; inoltre l'operatore "==" fornisce sicurezza in fase di compilazione e in fase di esecuzione.

Diamo prima uno sguardo alla sicurezza in fase di esecuzione nel seguente frammento in cui l'operatore "==" viene utilizzato per confrontare gli stati e non verrà generata un'eccezione NullPointerException se uno dei due valori è null . Al contrario, sarebbe generata un'eccezione NullPointerException se fosse stato utilizzato il metodo equals:

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

Per quanto riguarda la sicurezza in fase di compilazione , diamo un'occhiata a un altro esempio in cui un'enumerazione di un tipo diverso viene confrontata utilizzando il metodo equals è determinata essere vera, perché i valori di enum e del metodo getStatus coincidono casualmente, ma logicamente il il confronto dovrebbe essere falso. Questo problema viene evitato utilizzando l'operatore "==".

Il compilatore contrassegnerà il confronto come errore di incompatibilità:

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

4. Utilizzo di tipi di enumerazione nelle istruzioni Switch

I tipi enum possono essere utilizzati anche nelle istruzioni switch :

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

5. Campi, metodi e costruttori in enumerazioni

Puoi definire costruttori, metodi e campi all'interno di tipi enum che lo rendono molto potente.

Estendiamo l'esempio sopra e implementiamo la transizione da uno stadio di una pizza a un altro e vediamo come possiamo sbarazzarci dell'istruzione if e dell'istruzione switch usate prima:

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. } 

Lo snippet di prova riportato di seguito mostra come funziona:

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

6. EnumSet ed EnumMap

6.1. EnumSet

L'EnumSet è un specializzata Set implementazione pensato per essere utilizzato con Enum i tipi.

È una rappresentazione molto efficiente e compatta di un particolare Set di costanti Enum rispetto a un HashSet , grazie alla rappresentazione interna del vettore di bit utilizzata. E fornisce un'alternativa indipendente dai tipi ai tradizionali "bit flag" basati su int , permettendoci di scrivere codice conciso più leggibile e gestibile.

L'EnumSet è una classe astratta che ha due implementazioni chiamati RegularEnumSet e JumboEnumSet , uno dei quali viene scelto a seconda del numero di costanti nel enum al momento di istanziazione.

Pertanto è sempre una buona idea utilizzare questo set ogni volta che si desidera lavorare con una raccolta di costanti enum nella maggior parte degli scenari (come il sottoinsieme, l'aggiunta, la rimozione e per le operazioni di massa come containsAll e removeAll ) e utilizzare Enum.values ​​( ) se vuoi solo iterare su tutte le possibili costanti.

Nello snippet di codice riportato di seguito, puoi vedere come EnumSet viene utilizzato per creare un sottoinsieme di costanti e il suo utilizzo:

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. } 

L'esecuzione del seguente test ha dimostrato la potenza della EnumSet attuazione del Set di interfaccia:

@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 è un'implementazione Map specializzata pensata per essere utilizzata con costanti enum come chiavi. È un'implementazione efficiente e compatta rispetto alla sua controparte HashMap ed è rappresentata internamente come un array:

EnumMap map; 

Diamo una rapida occhiata a un esempio reale che mostra come può essere utilizzato nella pratica:

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; } 
      
     
    

L'esecuzione del seguente test ha dimostrato la potenza della EnumMap attuazione della mappa dell'interfaccia:

@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. Implementare modelli di progettazione utilizzando enumerazioni

7.1. Pattern Singleton

Normalmente, l'implementazione di una classe utilizzando il pattern Singleton non è affatto banale. Le enumerazioni forniscono un modo semplice e veloce per implementare i singleton.

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

In questo articolo, abbiamo esplorato l'enumerazione Java, dalle basi del linguaggio a casi d'uso più avanzati e interessanti nel mondo reale.

I frammenti di codice di questo articolo sono disponibili nel repository Github.