Un guide pour Java Enums

Un guide de Java Enums

1. Vue d'ensemble

Dans cet article, nous verrons ce que sont les énumérations Java, quels problèmes elles résolvent et comment utiliser certains des modèles de conception dans la pratique.

The enum keyword was introduced in Java 5. Il désigne un type spécial de classe qui étend toujours la classejava.lang.Enum. Pour la documentation officielle sur leur utilisation, jetez un œil auxdocumentation.

Les constantes définies de cette manière rendent le code plus lisible, permettent une vérification au moment de la compilation, documentent en amont la liste des valeurs acceptées et évitent les comportements inattendus dus à la transmission de valeurs non valides.

Voici un exemple simple et rapide d’énumération qui définit le statut d’une commande de pizza; l'état de la commande peut êtreORDERED,READY ouDELIVERED:

public enum PizzaStatus {
    ORDERED,
    READY,
    DELIVERED;
}

En outre, elles viennent 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 «==»

Comme les types enum garantissent qu’une seule instance des constantes existe dans la machine virtuelle, 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 “==” assure la sécurité lors de la compilation et de l'exécution.

Regardons d'abordat run-time safety dans l'extrait suivant où l'opérateur «==» est utilisé pour comparer les statuts et unNullPointerException ne sera pas renvoyé si l'une des valeurs estnull. Inversement, les anNullPointerException seraient lancés si la méthode equals était utilisée:

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

Quant àcompile time safety, jetons un œil à un autre exemple où une énumération d'un type différent est comparée à l'aide de la méthodeequals est déterminée comme vraie - car les valeurs de l'énumération et dugetStatusPar coïncidence, la méthode est la même, mais la comparaison devrait logiquement être fausse. Ce problème est évité en utilisant l'opérateur “==”.

Le compilateur signalera 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 les instructionsswitch:

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.

Prolongeons 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'instructionif et de l'instructionswitch 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 etEnumMap

6.1. EnumSet

LeEnumSet est une implémentationSet spécialisée destinée à être utilisée avec les typesEnum.

C'est une représentation très efficace et compacte d'unSet particulier des constantesEnum par rapport à unHashSet, en raison duBit Vector Representation interne qui est utilisé. Et il fournit une alternative de type sécurisé aux «bit flags» traditionnels basés surint, nous permettant d'écrire un code concis qui est plus lisible et maintenable.

LeEnumSet est une classe abstraite qui a deux implémentations appeléesRegularEnumSet etJumboEnumSet, 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 commecontainsAll etremoveAll) et utilisezEnum.values() si vous souhaitez simplement parcourir toutes les constantes possibles.

Dans l'extrait de code ci-dessous, vous pouvez voir commentEnumSet 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émentationEnumSet de l'interfaceSet:

@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émentationMap spécialisée destinée à être utilisée avec des constantes enum comme clés. C'est une implémentation efficace et compacte par rapport à son homologueHashMap 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émentationEnumMap de l'interfaceMap:

@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'énums

7.1. Motif singleton

Normalement, l'implémentation d'une classe en utilisant le modèle Singleton n'est pas triviale. Les énumérations fournissent un moyen simple et rapide d'implémenter des singletons.

En plus de cela, puisque la classe enum implémente l'interfaceSerializable sous le capot, la classe est garantie d'être un singleton par la JVM, ce qui contrairement à l'implémentation conventionnelle où nous devons nous assurer qu'aucune nouvelle instance n'est créée pendant désérialisation.

Dans l'extrait de code ci-dessous, nous voyons comment implémenter un modèle singleton:

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. Modèle de stratégie

Classiquement, le modèle Strategy est écrit en ayant une interface implémentée par différentes classes.

Ajouter une nouvelle stratégie signifiait ajouter une nouvelle classe d'implémentation. Avec les énumérations, ceci est réalisé avec moins d'effort, ajouter une nouvelle implémentation signifie définir une autre instance avec une implémentation quelconque.

L'extrait de code ci-dessous montre comment implémenter le modèle de stratégie:

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

Ajoutez la méthode suivante à la classePizza:

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 et Enums

La classePizza peut être réécrite en Java 8, et vous pouvez voir comment les méthodesgetAllUndeliveredPizzas() etgroupPizzaByStatus() deviennent si concises avec l'utilisation de lambdas et les APIStream:

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. Représentation JSON d'énum

En utilisant les bibliothèques Jackson, il est possible d’avoir une représentation JSON des types enum comme s’ils étaient des POJO. L'extrait de code ci-dessous montre les annotations de Jackson qui peuvent être utilisées pour les mêmes:

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

Nous pouvons utiliser Pizza et PizzaStatus comme suit:

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

pour générer la représentation JSON suivante de l'étatPizzas:

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

Pour plus d'informations sur la sérialisation / désérialisation JSON (y compris la personnalisation) des types enum, reportez-vous auxJackson – 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 plus avancés et intéressants du monde réel.

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