Enumérations – Utilisation avancée
Avant l'arrivée des énumérations, deux principaux choix s'offraient à vous pour effectuer des énumérations : les constantes et les classes de type N-gleton. C'était au développeur de choisir, selon le contexte, quelle solution semblait la plus adaptée à son problème.
Vous constaterez rapidement qu'aucune de ces solutions n'était satisfaisante.
Comment développiez-vous vos énumérations ?
Utilisation de constantes
Java ne permettant pas de créer des namespaces, les développeurs se contentaient, la plupart du temps, de simuler ceux-ci par l'intermédiaire d'une simple convention de nommage (ex : en utilisant le préfixe commun 'GENDER_') (exemple 1).
Même si ce problème était facilement soluble en regroupant les valeurs dans une interface (exemple 2), restait un problème majeur : les dangers liés au typage.
Les différentes valeurs étant typées 'int', aucune erreur n'était remontée lorsque vous passiez une 'PRIMARY_COLOR' en paramètre d'une méthode demandant un 'GENDER'. Les erreurs engendrées par ce comportement pouvaient être difficiles à détecter. C'est un peu comme si on se contentait de développer une classe en utilisant un seul type pour ses champs et de déléguer aux clients la bonne utilisation de celle-ci.
public static final int PRIMARY_COLOR_BLUE = 0;
public static final int PRIMARY_COLOR_RED = 1;
public static final int PRIMARY_COLOR_RED = 2;
// Exemple 2
interface Gender {
int MALE = 0;
int FEMALE = 1;
}
Utilisation d'un 'N-gleton'
Du coup, la meilleure solution pour les développeurs pour simuler les énumérations restait encore d'utiliser des inner classes statiques et de s'assurer manuellement de l'unicité de ces instances. Si nous voulons utiliser un typage fort et des fonctionnalités de haut niveau, on peut définir une classe dont les instances sont les valeurs du type énuméré, c'est plus fiable, mais un peu lourd !
L'idée est d'adapter le pattern singleton pour en faire un 'N-gleton'. Le nombre d'instances correspondant au nombre de valeurs différentes du type énuméré. On dispose alors de l'égalité, l'implémentation de la méthode equals() héritée de Object convient parfaitement. Malheureusement, il reste impossible d'utiliser les instances pour contrôler un switch/case.
public class Member {
// ... stuff of the member class
public static final class Gender {
// simulation des éléments de l'énumération
public static final Gender MALE = new Gender(0, "MALE");
public static final Gender FEMALE = new Gender(1, "FEMALE");
// constructeur privé
private final int ordinal;
private final String name;
private Gender(int ordinal, String name) {
this.ordinal = ordinal;
this.name = name;
}
// simulation de la méthode ordinal() disponible sur les énumérations depuis le jdk 5
public final int ordinal() {
return ordinal;
}
// simulation de la méthode name() disponible sur les énumérations depuis le jdk 5
public final String name() {
return name;
}
// simulation de la méthode toString() disponible sur les énumérations depuis le jdk 5
@Override
public String toString() {
return name();
}
// simulation de la méthode values() disponible sur les énumérations depuis le jdk 5
private static final Gender[] values = new Gender[] { MALE, FEMALE };
public static Gender[] values() {
return values;
}
// simulation de la méthode valueOf() disponible sur les énumérations depuis le jdk 5
public static Gender valueOf(String name) {
for (int i = 0; i <values.length; i++) {
if (values[i].name().equals(name)) {
return values[i];
}
}
return null;
}
}
// set the gender to the bean Member
private Gender gender;
public void setGender(Gender gender) {
this.gender = gender;
}
}
(Ne prenez pas la peine de me dire qu'il y avait plus simple ou plus performant, il ne s'agit que d'un exemple)
Utiliser les énumérations
Au premier abord, il est tentant de comparer les énumérations java avec ceux des autres langages. Elles sont bien plus puissantes que dans la plupart des autres langages. Là où C / C++ ou C# se contentent de traiter les énumérations comme une suite de nombres entiers, les enum Java se comportent comme de vraies classes. On peut tout à fait les comparer au N-gleton que nous venons de développer dans l'exemple précédent possédant un certain nombre de propriétés implicites.
- Chaque élément d'un
enumest représenté comme une instance unique d'une classe marquéefinaldont le constructeur n'est jamais rendu visible depuis l'extérieur. - L'utilisation d'un
enumen paramètre d'une méthode la rend type safe. Aucun élément autre que ceux d'une énumération particulière ne sera accepté en paramètre d'une méthode : on ne mélange pas les choux avec les carottes. - Chaque
enumoffre des fonctionnalités de base permettant de la convertir et de l'identifier. Par exemple, la méthodeordinal()renvoie l'index de l'élément dans la liste ; la méthodename()renvoie le nom de l'élément ; la méthodevalues()renvoie un tableau contenant l'ensemble ordonné des éléments de l'énumération ; la méthodevalueOf()permet de récupérer un élément de l'énumération à partir d'une chaîne de caractères - Chaque
enumfournie une implémentation implicite et correcte des méthodes définies dansObject. Il est possible d'utiliser==pour comparer deux instances d'unenum. Par défaut, la méthodetoString()renvoie le nom de l'élément tel qui a été écrit lors de sa déclaration. - Chaque
enumimplémente implicitement les interfacesComparableetSerializable. Comme elles se comportent comme de simples classes, il est également possible de leur faire implémenter nos propres interfaces.
Il est possible de déclarer les enum soit en top-level class, soit en inner class. En général, si une énumération est assez générique pour être utilisée à plusieurs endroits, il est préférable de la déclarer en top-level dans un fichier séparé.
Côté performance, dans la plupart des cas, les énumérations n'ont rien à envier aux listes de valeurs constantes. Ce n'est bien entendu pas magique-, si on pinaille,- : on note effectivement des pertes mineures, en taille et en temps de chargement. Ces pertes sont négligeables dans la plupart des contextes.
public class Member {
// ... stuff of the member class
// déclaration de l'énumération quasi équivalent à l'exemple 3 utilisant une inner classe
public static enum Gender { MALE, FEMALE; }
// set the gender to the bean Member
private Gender gender;
public void setGender(Gender gender) {
this.gender = gender;
}
}
Lier des données spécifiques pour chaque instance d'une énumération
Il est possible de lier des données spécifiques pour chaque instance d'une énumération. Pour cela, il suffit de déclarer des champs comme vous le feriez dans une classe traditionnelle et de les affecter via un constructeur de la façon suivante (exemple 5). Chaque champ peut (devrait) être marqué final, puisqu'il ne devrait être affecté qu'une seule fois lors de l'initialisation de l'enum. Du coup, ces champs finaux pourraient être déclarés public pour donner un accès direct aux utilisateurs, mais il est préférable de les laisser private et d'ajouter des accesseurs traditionnels.
public enum Operation {
// élements de l'énumération avec des données attachées
PLUS("+"), MINUS("-"), TIMES("*"), DIVIDE("/");
// constructeur privé affectant le symbole au champ local
private final String symbol;
private Operation(String symbol) {
this.symbol = symbol;
}
// getter traditionnel
public String getSymbol() {
return symbol;
}
}
Ajouter / surcharger des méthodes à une énumération
Les énumérations étant comparables à des classes, il est possible de leur ajouter des méthodes static ou non après la déclaration des éléments de l'enum. Dans la pratique, c'est d'ailleurs souvent le cas, on commence par déclarer une simple liste d'éléments. Puis on l'étoffe petit à petit jusqu'à obtenir un 'gros objet' contenant des données et des comportements particuliers à certaines situations.
Reprenons notre exemple précédent et rajoutons une méthode d'instance permettant d'effectuer l'opération.
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// méthode questionnable en fonction de l'instance courante
public double calculate(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Opération inconnue : " + this);
}
}
Vous aurez noté que ce code reste fragile. Si vous ajoutez un élément à l'énumération, celle-ci compilera toujours. Par conséquent, vous vous rendrez compte de votre oubli au runtime lorsque l'application échouera.
Essayons de l'améliorer un peu, pour cela, nous allons utiliser l'héritage en ligne. En effet, s'il n'est pas possible de faire hériter une énumération dans son ensemble ("public enum MyEnum2 extends MyEnum1" n'est pas valide), il est possible, pour chaque élément de celui-ci, de surcharger une ou plusieurs méthodes en utilisant un héritage anonyme.
public enum Operation {
// Liste d'élements de l'énumération implémentant la méthode abstraite
PLUS {
@Override
public double calculate(double x, double y) {
return x + y;
}
},
MINUS { @Override public double calculate(double x, double y) { return x - y; } },
TIMES { @Override public double calculate(double x, double y) { return x * y; } },
DIVIDE { @Override public double calculate(double x, double y) { return x / y; } };
// méthode abstraite, à implémenter pour chaque élement de l'énum
public abstract double calculate(double x, double y);
}
Avec cette implémentation, si vous rajoutez un élément dans l'énumération, le compilateur vous avertira si vous oubliez d'implémenter la méthode abstraite. Pourtant, elle comporte encore des défauts : il est très difficile de partager une implémentation de méthode entre différents éléments de l'énumération. Contrairement au switch qui nous permettait de réutiliser du code en séquençant les case et / ou en utilisant default, le partage du code n'est plus possible via l'héritage anonyme. Il est bien entendu possible de dupliquer le code ou d'externaliser celui-ci dans un helper, mais ces deux solutions complexifient le code et le rendent plus difficile à lire. À terme, elles facilitent l'arrivée de bugs et complexifient la maintenance.
Ce que nous désirons réellement, c'est un moyen simple de pouvoir choisir (et de forcer à choisir) entre plusieurs implémentations lors de l'ajout d'un nouvel élément à une énumération. Il est temps de dévoiler notre ultime botte secrète : plus lourd, mais aussi plus sûr et plus flexible que les solutions précédentes, voici le strategy enum pattern !
public enum SuperHero {
// la sélection d'un genre est rendue obligatoire lors de l'ajout
// d'un élément, il est ainsi possible de partager / réutiliser
// plusieurs implémentations d'une méthode.
BATMAN(Gender.MALE), WONDER_WOMAN(Gender.FEMALE), SPIDERMAN(Gender.MALE);
// constructeur affectant un élément de l'inner enum
private final Gender gender;
private SuperHero(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return "My name is " + name() + " ! ";
}
// affiche le nom du super héro et un messge en fonction de son genre.
public String introduceMe() {
return toString() + gender.whoiam();
}
// INNER ENUM
// utilisé pour partager des implémentations
// entre différents super héros
private enum Gender {
MALE {
@Override
public String whoiam() {
return "I am a boy !";
}
},
FEMALE {
@Override
public String whoiam() {
return "I am a girl !";
}
};
// méthode abstraite : doit être implémentée
public abstract String whoiam();
}
}
Je vous avais prévenu, on est bien loin d'une énumération traditionnelle. Vue la quantité de code à fournir, inutile de vous dire que vous n'avez peut-être pas besoin de ce pattern pour chacune de vos énumérations...
Best / Worst practices
Proscrire l'utilisation de la méthode ordinal()
Comme nous l'avons vu plus haut, chaque énumération fournit une méthode ordinal() implicite permettant d'identifier la position d'un élément dans l'énumération. Au premier abord, nous sommes tentés d'utiliser cette méthode pour identifier un élément de manière unique. Hibernate nous propose d'ailleurs d'utiliser celle-ci pour persister nos champs énumérés. Il s'avère qu'il s'agit en général d'une bien mauvaise idée. La javadoc est d'ailleurs très claire à ce sujet : "Cette méthode n'a aucune utilité pour la plupart des développeurs".
Pour illustrer ce problème, prenons l'exemple d'une énumération représentant un ensemble fini de formes gérées par un système. Ajoutons à cette énumération une méthode permettant de calculer le nombre de segments de chaque forme.
public enum Forme {
CERCLE, ARC, TRIANGLE, CARRE, PENTAGONE;
// DANGER : calvaire en maintenance
public int getNbSegments() {
return ordinal() + 1;
}
}
L'utilisation de la méthode ordinal() est ici une bien mauvaise idée. Que se passe-t-il si l'on désire ajouter le losange au système ? La méthode getNbSegements() ne fonctionne plus du tout. Pour une si petite modification, le coût de maintenance est démesuré, une restructuration complète de l'énumération est nécessaire. De plus, ici encore, le problème n'est remonté qu'au runtime : il y a un risque de partir en production sans même s'apercevoir que la méthode getNbSegements() ne fonctionne plus.
Pour éviter ce genre de problème, il est préférable d'utiliser un champ personnalisé.
public enum Forme {
// Eléments contenant une position définit par l'utilisateur
// Ici, on n'a préféré ne pas utiliser ordinal()
CERCLE(1), ARC(2), TRIANGLE(3), LOSANGE(4), CARRE(4), PENTAGONE(5);
// constructeur affectant la position
private final int nbSegments;
private Forme(int nbSegments) {
this.nbSegments = nbSegments;
}
// getter
public int getNbSegments() {
return nbSegments;
}
}
Quand utiliser les switch avec les énumérations ?
Nous avons vu précédemment dans cet article qu'utiliser un switch au sein d'une méthode d'une énumération sur ses différentes valeurs n'est pas idéal. S'il est dangereux d'utiliser les switch sur les énumérations, il est légitime de se demander s'il est bon de les utiliser ailleurs dans le code. La réponse est oui : ouf, on était à deux doigts de faire une pétition à Sun pour retirer cette fonctionnalité !
Si nous voulons modifier ou rajouter du comportement à une énumération sur laquelle nous n'avons pas la main, nous n'avons d'autres choix que de tester au cas sur quel élément de l'énumération sommes-nous.
Si nous reprenons l'exemple 7 déclarant une liste d'opérations et que nous voulons y ajouter une méthode permettant de récupérer l'inverse d'une opération, le code, externalisé dans un helper, à de fortes chances de ressembler à ça :
public static Operation inverse(Operation operation) {
switch(operation) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new IllegalArgumentException("Opération inconnue : " + operation.name());
}
}
Comment obtenir efficacement un élément d'une énumération ?
Vous pouvez charger un élément d'une énumération à partir de son nom (name()) en utilisant la méthode implicite valueOf(String). Par exemple, l'appel Operation.valueOf("TIMES") ; retournera l'instance correspondante de l'énumération.
Maintenant, il est souvent nécessaire de pouvoir effectuer cette même opération sur un autre champ (données personnalisées). Pour cela vous n'avez d'autre solution que d'effectuer le traitement manuellement.
public static enum Operation {
// éléments avec un symbole
PLUS("+"), MINUS("-"), TIMES("*"), DIVIDE("/");
// constructeur
private final String symbol;
private Operation(String symbol) {
this.symbol = symbol;
}
// getter
public String getSymbol() {
return symbol;
}
// map permettant de récupérer une Operation avec un symbole
private static final Map<String, Operation> mapSymbols = new HashMap<String, Operation>();
static {
for (Operation operation : values()) {
mapSymbols.put(operation.getSymbol(), operation);
}
}
// récupération de l'instance
public static Operation fromSymbol(String symbole) {
final Operation value = mapSymbols.get(symbole);
if (value != null) {
return value;
}
throw new IllegalArgumentException("Symbole incconu : " + symbole);
}
}
L'idée est de déléguer le travail à une map contenant l'ensemble des valeurs de l'énumération dont la clé représente les données permettant d'identifier les différentes instances. Ainsi, lorsque la méthode est appelée, il ne reste qu'à retourner directement le contenu de la map pour la donnée passée en paramètre.
Utiliser des EnumSet à la place de bit fields
Il n'est pas rare de tomber sur du code contenant des listes de constantes entières représentées par des puissances de 2. Champs champ de bits, très utilisés dans le monde du langage C, permettent de stocker plusieurs constantes au sein d'un même entier.
public class Text {
public static final int STYLE_BOLD = 1 <<0; // 1
public static final int STYLE_ITALIC = 1 <<1; // 2
public static final int STYLE_UNDERLINE = 1 <<2; // 4
// appelez la méthode avec un ou plusieurs styles séparés par des OU
// exemple : applyStyles(STYLE_BOLD | STYLE_ITALIC)
public void applyStyles(int styles) { ... }
}
Les énumérations les rendent obsolètes. Il est préférable de remplacer les champs de bits non typés et peu lisibles par des enum. L'ellipse (méthode à nombre variable d'arguments) facilite l'appel de la l'utilisation de la méthode, mais reste moins performant qu'un champ de bits. C'est pourquoi certains recommandent l'utilisation d'un EnumSet. Ils sont spécialement optimisés pour ce type de traitement : pour les énumérations contenant moins de 64 (!) éléments, l'EnumSet est représenté en mémoire sous forme d'un seul long offrant ainsi des performances à peu près équivalentes à un champ de bits traditionnel tout en gardant les avantages offerts pas les enum.
public class Text {
public static enum Style { BOLD, ITALIC, UNDERLINE; }
// version Elipse : accepte les doublons
// appelez la méthode avec zéro, un ou plusieurs styles
public void applyStyles(Style... styles) { }
// version EnumSet : filtre les doublons
// exemple d'appel : applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC))
public void applyStyles(EnumSet<Style> styles) { }
}
Pour conclure, quand faut-il utiliser les énumérations ?
La mauvaise notoriété des énumérations provient des excès d'utilisation souvent constatés. Certains débutants n'hésitent pas à utiliser des enum à tort et à travers. À l'opposé de cette maladie grave (la "Enum-érol" aiguë) prennent place les hordes d'"Enum-érophobes". Certains les décrivent comme des preuves indéniables d'une mauvaise conception, d'autres comme un fléau au long terme : peu performants, trop statiques. Qui n'est jamais tombé sur ce fameux DBA qui refuse de transformer la colonne 'genre' d'une table 'membre' en énumération sous prétexte que cela sera difficile à modifier si on décide d'ajouter un 3e sexe ? Je n'arriverai certainement pas à persuader tout le monde ici, mais essayons tout de même de rappeler quelques règles élémentaires d'utilisation d'une énumération.
Envisagez d'utiliser une énumération lorsque :
- vous utilisez une énumération naturelle (jour de la semaine / sexe)
- vous avez besoin d'un set de constantes
- vous utilisez un set de valeur connues dès la compilation
Pour finir, rappelons que les énumérations rendent le code plus lisible, autodocumenté, type-safe et plus évolutif que l'utilisation d'une simple liste de constantes. Cet article a décrit une utilisation avancée des énumérations, il ne faut pas oublier que dans la plupart des cas leur déclaration peut rester sous sa forme la plus simple : sans constructeur, ni données ou comportement attaché.
Quelques liens :
- Manière élégante de faire des singletons avec des énumérations.
- Utilisation d’une énumération pour jouer avec des fichiers XML : à chaque type de nœud du fichier XML correspond un élément de l’énumération ; chaque élément implémente une méthode abstraite permettant d’effectuer les traitements spécifiques à chacun des nœuds ; l’utilisation de la méthode
valueOf()simplifie les appels aux différents traitements.









micro remarque:
la méthode valueOf() permet de récupérer un élément de l'énumération à partir une chaîne de caractères
- manque le "d'"
Sinon, excellent article (comme souvent!) mais qui en est l'auteur?
La coquille est corrigée.
Merci pour les compliments.
Bravo, excellent article ! Il est rare de lire des billets aussi clairs et aussi complets sur un aspect technique. Je conserve celui-ci en référence.
Erwan, juste une petite remarque :
on voit
"De plus, ici encore, le problème n'est remonté qu'au runtime"
Tout dépend de quel runtime on parle.
Si c'est pendant les tests unitaires, pas de problème ...
Bonjour Mike, je n’ai pas dû comprendre votre commentaire puisque la fin de cette même phrase explicite ce sujet : ‘… il y a un risque de partir en production …’.
De plus, ce problème est aussi dommageable lors de l’exécution des tests unitaires : soient ceux-ci couvrent correctement la méthode
getNbSegements()et le problème sera facilement repéré/résolu ; soit le bug à de fortes chances de partir en production en l’état.[...] trouver un consensus sur ce sujet polémique qui n’est pas sans nous rappeler d’autres critiques similaires. Tags: annotation, dmServer, Flash, Flex, Java, jdk-5, Méthodes agiles, Netbeans, RCP, [...]