Guide des énumérations Java

1. Vue d'ensemble

Dans cet article, nous verrons ce que sont les énumérations Java, les problèmes qu'elles résolvent et comment certains des modèles de conception peuvent être utilisés dans la pratique.

Le mot clé enum a été introduit dans Java 5. Il désigne un type spécial de classe qui étend toujours la classe java.lang.Enum . Pour la documentation officielle sur leur utilisation, consultez la documentation.

Les constantes définies de cette manière rendent le code plus lisible, permettent la vérification à la compilation, documentent à l'avance la liste des valeurs acceptées et évitent tout comportement inattendu dû à la transmission de valeurs non valides.

Voici un exemple simple et rapide d'énumération qui définit le statut d'une commande pour une pizza; l'état de la commande peut être COMMANDÉ , PRÊT ou LIVRÉ :

public enum PizzaStatus { ORDERED, READY, DELIVERED; }

De plus, ils sont livrés avec de nombreuses méthodes utiles, que vous auriez sinon à écrire vous-même si vous utilisiez des constantes finales statiques publiques traditionnelles.

2. Méthodes d'énumération personnalisées

OK, maintenant que nous avons une compréhension de base de ce que sont les énumérations et comment vous pouvez les utiliser, prenons notre exemple précédent au niveau suivant en définissant des méthodes API supplémentaires sur l'énumération:

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. Comparaison des types Enum à l'aide de l'opérateur «==»

Étant donné que les types enum garantissent qu'une seule instance des constantes existe dans la JVM, nous pouvons utiliser en toute sécurité l'opérateur «==» pour comparer deux variables comme indiqué dans l'exemple ci-dessus; de plus, l'opérateur «==» offre une sécurité à la compilation et à l'exécution.

Examinons d'abord la sécurité à l' exécution dans l'extrait de code suivant où l'opérateur «==» est utilisé pour comparer les statuts et une NullPointerException ne sera pas lancée si l'une ou l'autre des valeurs est nulle . Inversement, une NullPointerException serait lancée si la méthode equals était utilisée:

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

En ce qui concerne la sécurité de la compilation , jetons un coup d'œil à un autre exemple où une énumération d'un type différent est comparée à l'aide de la méthode equals est déterminée comme vraie - parce que les valeurs de l'énumération et de la méthode getStatus sont par coïncidence les mêmes, mais logiquement le la comparaison doit être fausse. Ce problème est évité en utilisant l'opérateur «==».

Le compilateur marquera la comparaison comme une erreur d'incompatibilité:

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

4. Utilisation des types Enum dans les instructions Switch

Les types Enum peuvent également être utilisés dans une instruction switch :

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

5. Champs, méthodes et constructeurs dans les énumérations

Vous pouvez définir des constructeurs, des méthodes et des champs à l'intérieur de types enum qui le rendent très puissant.

Étendons l'exemple ci-dessus et implémentons la transition d'une étape d'une pizza à une autre et voyons comment nous pouvons nous débarrasser de l' instruction if et de l' instruction switch utilisées auparavant:

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

L'extrait de test ci-dessous montre comment cela fonctionne:

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

6. EnumSet et EnumMap

6.1. EnumSet

Le EnumSet est une société spécialisée Set mise en œuvre destiné à être utilisé avec Enum types.

C'est une représentation très efficace et compacte d'un ensemble particulier de constantes Enum par rapport à un HashSet , en raison de la représentation de vecteur de bit interne qui est utilisée. Et il fournit une alternative de type sûr aux «indicateurs de bits» traditionnels basés sur int , nous permettant d'écrire un code concis qui est plus lisible et maintenable.

Le EnumSet est une classe abstraite qui a deux implémentations appelées RegularEnumSet et JumboEnumSet , dont l'une est choisie en fonction du nombre de constantes dans l'énumération au moment de l' instanciation.

Par conséquent, c'est toujours une bonne idée d'utiliser cet ensemble chaque fois que nous voulons travailler avec une collection de constantes d'énumération dans la plupart des scénarios (comme le sous-ensemble, l'ajout, la suppression et pour les opérations en bloc comme containsAll et removeAll ) et utiliser Enum.values ​​( ) si vous souhaitez simplement parcourir toutes les constantes possibles.

Dans l'extrait de code ci-dessous, vous pouvez voir comment EnumSet est utilisé pour créer un sous-ensemble de constantes et son utilisation:

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'exécution du test suivant a démontré la puissance de l' implémentation EnumSet de l' interface 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 est une implémentation de Map spécialisée destinée à être utilisée avec des constantes enum comme clés. Il s'agit d'une implémentation efficace et compacte par rapport à son homologue HashMap et est représentée en interne sous forme de tableau:

EnumMap map; 

Jetons un coup d'œil à un exemple réel qui montre comment il peut être utilisé dans la pratique:

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'exécution du test suivant a démontré la puissance de l' implémentation EnumMap de l' interface 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. Implémenter des modèles de conception à l'aide d'énumérations

7.1. Motif singleton

Normalement, l'implémentation d'une classe à l'aide du modèle Singleton n'est pas triviale. Les énumérations fournissent un moyen simple et rapide d'implémenter des 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

Dans cet article, nous avons exploré l'énumération Java, des bases du langage aux cas d'utilisation réels plus avancés et intéressants.

Des extraits de code de cet article se trouvent dans le référentiel Github.