Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | Elémentaire |
Souvent lors de l'écriture de code, il est utile de pouvoir définir un ensemble fini de valeurs d'une donnée ; par exemple, pour définir les valeurs possibles qui vont caractériser l'état de cette donnée.
Pour cela, le type énumération permet de définir un ensemble de constantes : une énumération est un ensemble fini d'éléments constants. Cette fonctionnalité existe déjà dans les langages C et Delphi, entre autres.
Jusqu'à la version 1.4 incluse, la façon la plus pratique pour palier le manque du type enum était de créer des constantes dans une classe.
Exemple : |
public class FeuTricolore {
public static final int VERT = 0;
public static final int ORANGE = 1;
public static final int ROUGE = 2;
}
|
Cette approche fonctionne : les constantes peuvent être sérialisées et utilisées dans une instruction switch mais leur mise en oeuvre n'est pas type safe. Rien n'empêche d'affecter une autre valeur à la donnée de type int qui va stocker une des valeurs constantes.
A défaut, cette solution permet de répondre au besoin mais elle présente cependant quelques inconvénients :
Java 5 apporte un nouveau type nommé enum qui permet de définir un ensemble de champs constants. Cette nouvelle fonctionnalité est spécifiée dans la JSR 201.
Un exemple classique est l'énumération des jours de la semaine.
Exemple : |
public enum Jour {
LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE
}
|
Les énumérations permettent de définir un ensemble fini de constantes, chacune d'entre-elles est séparée des autres par une virgule. Comme ces champs sont constants, leur nom est en majuscule par convention.
Ce chapitre contient plusieurs sections :
La définition d'une énumération ressemble à celle d'une classe avec quelques différences :
Une énumération peut prendre plusieurs formes et être enrichie de fonctionnalités puisqu'une énumération est une classe Java.
Dans sa forme la plus simple, la déclaration d'une énumération se résume à définir l'ensemble des constantes.
Exemple : |
public enum FeuTricolore {
VERT, ORANGE, ROUGE
}; |
Les énumérations peuvent être déclarées à plusieurs niveaux. Le mot clé enum est au même niveau que le mot clé class ou interface : une énumération peut donc être déclarée au même endroit qu'une classe ou une interface, que cela soit dans un fichier dédié ou dans le fichier d'une autre classe.
Exemple : |
public class TestEnum2 {
public enum MonStyle {
STYLE_1, STYLE_2, STYLE_3, STYLE_4, STYLE_5
};
public static void main(String[] args) {
afficher(MonStyle.STYLE_2);
}
public static void afficher(MonStyle style) {
System.out.println(style);
}
} |
Lors de la compilation de cet exemple, une classe interne est créée pour encapsuler l'énumération.
Résultat : |
C:\java\workspace\TestEnum\bin>dir
Le volume dans le lecteur C s'appelle Disque_C
Le numéro de série du volume est 043F-2ED6
Répertoire de C:\java\workspace\TestEnum\bin
15/07/2010 16:33 <REP> .
15/07/2010 16:33 <REP> ..
15/07/2010 16:39 1 160 TestEnum2$MonStyle.class
15/07/2010 16:39 743 TestEnum2.class
2 fichier(s) 1 903 octets
2 Rép(s) 23 175 589 888 octets libres |
Les modificateurs d'accès s'appliquent à une énumération.
L'outil Javadoc recense les énumérations dans le fichier package-summary.html.
Le nom utilisé dans la déclaration de l'énumération peut être utilisé comme n'importe quelle classe dans la déclaration d'un type.
Une fois définie, il est possible d'utiliser l'énumération simplement en définissant une variable du type de l'énumération.
Exemple : |
public class TestEnum {
Jour jour;
public TestEnum(Jour jour) {
this.jour = jour;
}
public void afficherJour() {
switch (jour) {
case LUNDI:
System.out.println("Lundi");
break;
case MARDI:
System.out.println("Mardi");
break;
case MERCREDI:
System.out.println("Mercredi");
break;
case JEUDI:
System.out.println("Jeudi");
break;
case VENDREDI:
System.out.println("Vendredi");
break;
case SAMEDI:
System.out.println("Samedi");
break;
case DIMANCHE:
System.out.println("Dimanche");
break;
}
}
public static void main(String[] args) {
TestEnum testEnum = new TestEnum(Jour.SAMEDI);
testEnum.afficherJour();
}
} |
Lors de l'utilisation d'une constante, son nom doit être préfixé par le nom de l'énumération sauf dans le cas de l'utilisation dans une instruction switch.
Les énumérations étant transformées en une classe par le compilateur, ce dernier effectue une vérification de type lors de l'utilisation de l'énumération..
L'instruction switch a été modifiée pour permettre de l'utiliser avec une énumération puisque bien qu'étant physiquement une classe, celle-ci possède une liste finie de valeurs associées.
L'utilisation d'une énumération dans l'instruction switch impose de n'utiliser que le nom de la valeur sans la préfixer par le nom de l'énumération sinon une erreur est émise par le compilateur.
Exemple : |
switch(feu) {
case (FeuTricolore.VERT) :
System.out.println("passer");
break;
default :
System.out.println("arreter");
break;
} |
Résultat : |
Feu.java:24: an enum switch case label must be the unqualified name of an enumeration constant |
Chaque élément d'une énumération est associé à une valeur par défaut, qui débute à zéro et qui est incrémentée de un en un. La méthode ordinal() permet d'obtenir cette valeur.
Exemple : |
FeuTricolore feu = FeuTricolore.VERT;
System.out.println(feu.ordinal()); |
Il y a plusieurs avantages à utiliser les enums à la place des constantes notamment le typage fort et le préfixe de l'élément par le nom de l'énumération.
Une énumération peut mettre en oeuvre la plupart des fonctionnalités et des comportements d'une classe :
Un type enum hérite implicitement de la classe java.lang.Enum : il ne peut donc pas hériter d'une autre classe.
Exemple : |
public enum MonEnum extends Object {
UN, DEUX, TROIS;
}
|
Résultat : |
MyType.java:3: '{' expected
public enum MonEnum extends Object {
MonEnum.java:6: expected
2 errors |
Chacun des éléments de l'énumération est instancié par le constructeur sous la forme d'un champ public static.
Si les éléments de l'énumération sont définis sans argument alors un constructeur sans argument doit être proposé dans la définition de l'énumération (celui-ci peut être le constructeur par défaut si aucun autre constructeur n'est défini).
Le fait qu'une énumération soit une classe permet de définir un espace de nommage pour ses éléments ce qui évite les collisions, par exemple Puissance.ELEVEE et Duree.ELEVEE.
A partir du code source de l'énumération, le compilateur va générer une classe enrichie avec certaines fonctionnalités.
Exemple : |
C:\Users\Jean Michel\workspace\TestEnum\bin>javap FeuTricolore
Compiled from "FeuTricolore.java"
public final class FeuTricolore extends java.lang.Enum{
public static final FeuTricolore VERT;
public static final FeuTricolore ORANGE;
public static final FeuTricolore ROUGE;
static {};
public static FeuTricolore[] values();
public static FeuTricolore valueOf(java.lang.String);
}
|
La classe compilée a été enrichie automatiquement par le compilateur qui a identifié l'entité comme une énumération grâce au mot clé enum :
Le compilateur ajoute automatiquement certaines méthodes à une classe de type enum lors de la compilation, notamment les méthodes statiques :
Le nom fourni en paramètre de la méthode valueOf() doit correspondre exactement à l'identifiant utilisé dans la déclaration de l'énumération. Il n'est pas possible de redéfinir la méthode valueOf().
Une énumération propose une implémentation par défaut de la méthode toString() : par défaut, elle renvoie le nom de la constante. Il est possible de la redéfinir au besoin.
Il est possible de préciser une valeur pour chaque élément de l'énumération lors de sa définition : celle-ci sera alors stockée et pourra être utilisée dans les traitements.
Exemple : |
public enum Coefficient {
UN(1), DEUX(2), QUATRE(4);
private final int valeur;
private Coefficient(int valeur) {
this.valeur = valeur;
}
public int getValeur() {
return this.valeur;
}
}
|
Dans ce cas, l'énumération doit implicitement définir :
Attention : toutes les données manipulées dans un élément d'une énumération doivent être immuables. Par exemple, il ne faut pas encapsuler dans un élément d'une énumération une donnée dont la valeur peut fluctuer dans le temps puisque l'élément est instancié une seule et unique fois.
Il faut obligatoirement définir les constantes en premier, avant toute définition de champs ou de méthodes. Si l'enum contient des champs et/ou des méthodes, il est impératif de terminer la définition des contantes par un point virgule.
Il est aussi possible de fournir plusieurs valeurs à un élément de l'énumération : comme une énumération est une classe, il est possible d'associer plusieurs valeurs à un élément de l'énumération. Ces valeurs seront stockées sous la forme de propriétés et l'énumération doit fournir un constructeur qui doit accepter en paramètre les valeurs de chaque propriété.
Exemple : |
import java.math.BigDecimal;
public enum Remise {
COURANTE(new BigDecimal("0.05"), "Remise de 5%"),
FIDELITE(new BigDecimal("0.07"), "Remise de 7%"),
EXCEPTIONNELLE(new BigDecimal("0.10"), "Remise de 10%");
private final BigDecimal taux;
private final String libelle;
private Remise(BigDecimal taux, String libelle) {
this.taux = taux;
this.libelle = libelle;
}
public BigDecimal getTaux() {
return this.taux;
}
public String getLibelle() {
return this.libelle;
}
public BigDecimal calculer(BigDecimal valeur) {
return valeur.multiply(taux).setScale(2, BigDecimal.ROUND_FLOOR);
}
public static void main(String[] args) {
BigDecimal montant = new BigDecimal("153.99");
for (Remise remise : Remise.values()) {
System.out.println(remise.getLibelle() + " \t"
+ remise.calculer(montant));
}
}
}
|
Résultat : |
Remise de 5% 7.69
Remise de 7% 10.77
Remise de 10% 15.39 |
Dans l'exemple précédent, chaque constante est définie avec les deux paramètres qui la compose : le taux et le libellé. Ces valeurs sont passées au constructeur par le bloc d'initialisation static qui est créé par le compilateur.
Le constructeur d'une classe de type enum ne peut pas être public car il ne doit être invoqué que par la classe elle-même pour créer les constantes définies au début de la déclaration de l'énumération.
Un élément d'une énumération ne peut avoir que la valeur avec laquelle il est défini dans sa déclaration. Ceci justifie que le constructeur ne soit pas public et qu'une énumération ne puisse pas avoir de classes filles.
Tous les éléments de l'énumération sont encapsulés dans une instance finale du type de l'énumération : ce sont donc des singletons. De plus, les valeurs peuvent être testées avec l'opérateur == puisqu'elles sont déclarées avec le modificateur final.
Plusieurs fonctionnalités permettent de s'assurer qu'il n'y aura pas d'autres instances que celles définies par le compilateur à partir du code source :
Une énumération peut implémenter une ou plusieurs interfaces. Comme une énumération est une classe, elle peut aussi contenir une méthode main().
Le type Enum de Java est plus qu'une simple liste de constantes car une énumeration est définie dans une classe. Une classe de type Enum peut donc contenir des champs et des méthodes dédiées.
Pour encore plus de souplesse, il est possible de définir chaque élément sous la forme d'une classe interne dans laquelle on fournit une implémentation particulière pour chaque élément.
Il est ainsi possible de définir explicitement, pour chaque valeur, le corps de la classe qui va l'encapsuler. Une telle définition est similaire à la déclaration d'une classe anonyme. Cette classe est implicitement une extension de la classe englobante. Il est ainsi possible de redéfinir une méthode de l'énumération.
Exemple : |
import java.math.BigDecimal;
public enum Remise {
COURANTE(new BigDecimal("0.05"), "Remise de 5%") {
@Override
public String toString() {
return "Remise 5%";
}
},
FIDELITE(new BigDecimal("0.07"), "Remise de 7%") {
@Override
public String toString() {
return "Remise fidelite 7%";
}
},
EXCEPTIONNELLE(new BigDecimal("0.10"), "Remise de 10%") {
@Override
public String toString() {
return "Remise exceptionnelle 10%";
}
@Override
public String getLibelle() {
return "Remise à titre exceptionnel de 10%";
}
};
private final BigDecimal taux;
private final String libelle;
private Remise(BigDecimal taux, String libelle) {
this.taux = taux;
this.libelle = libelle;
}
public BigDecimal getTaux() {
return this.taux;
}
public String getLibelle() {
return this.libelle;
}
public BigDecimal calculer(BigDecimal valeur) {
return valeur.multiply(taux).setScale(2, BigDecimal.ROUND_FLOOR);
}
public static void main(String[] args) {
BigDecimal montant = new BigDecimal("153.99");
for (Remise remise : Remise.values()) {
System.out.println(remise + " \t" + remise.calculer(montant));
}
}
}
|
Résultat : |
Remise 5% 7.69
Remise fidelite 7% 10.77
Remise exceptionnelle 10% 15.39 |
Il est aussi possible de définir une méthode abstract dans l'énumération pour forcer la définition de la méthode dans chaque élément.
Exemple : |
import java.math.BigDecimal;
public enum Remise {
COURANTE(new BigDecimal("0.05"), "Remise de 5%") {
@Override
public String getLibelle() {
return this.libelle;
}
},
FIDELITE(new BigDecimal("0.07"), "Remise de 7%") {
@Override
public String getLibelle() {
return "Remise fidélité de 7%";
}
},
EXCEPTIONNELLE(new BigDecimal("0.10"), "Remise de 10%") {
@Override
public String getLibelle() {
return "Remise à titre exceptionnel de 10%";
}
};
private final BigDecimal taux;
protected final String libelle;
private Remise(BigDecimal taux, String libelle) {
this.taux = taux;
this.libelle = libelle;
}
public BigDecimal getTaux() {
return this.taux;
}
public abstract String getLibelle();
public BigDecimal calculer(BigDecimal valeur) {
return valeur.multiply(taux).setScale(2, BigDecimal.ROUND_FLOOR);
}
public static void main(String[] args) {
BigDecimal montant = new BigDecimal("153.99");
for (Remise remise : Remise.values()) {
System.out.println(remise.getLibelle() + " \t" + remise.calculer(montant));
}
}
}
|
Résultat : |
Remise de 5% 7.69
Remise fidélité de 7% 10.77
Remise à titre exceptionnelle de 10% 15.39 |
Il faut cependant utiliser cette possibilité avec parcimonie car le code est moins lisible.
La mise en oeuvre des énumérations présente plusieurs limitations.
L'ordre de définition du contenu de l'énumération est important : les éléments de l'énumération doivent être définis en premier.
Un élément d'une énumération ne doit pas être null.
Un type Enum hérite implicitement de la classe java.lang.Enum : il ne peut pas hériter d'une autre classe mère.
Pour garantir qu'il n'y ait qu'une seule instance d'un élément d'une énumération, le constructeur n'est pas accessible et l'énumération ne peut pas avoir de classe fille.
Une énumération ne peut pas être définie localement dans une méthode.
La méthode values() renvoie un tableau des éléments de l'énumération dans l'ordre dans lequel ils sont déclarés mais il ne faut surtout pas utiliser l'ordre des éléments d'une énumération dans les traitements : il ne faut par exemple pas tester la valeur retournée par la méthode ordinal() dans les traitements. Des problèmes apparaitront à l'exécution si l'ordre des éléments est modifié car le compilateur ne peut pas détecter ce type de changement.
Il n'est pas possible de personnaliser la sérialisation d'une énumération en redéfinissant les méthodes writeObject() et writeReplace() qui seront ignorées lors de la sérialisation. De plus, la déclaration d'un serialVersionUlD est ignorée car sa valeur est toujours 0L.
Jusqu'à Java 15, il n'est pas possible de définir des énumérations locales.
Exemple ( code Java 15 ) : |
public class TestEnumLocale {
public void traiter() {
enum Statut { VALIDE, INVALIDE };
}
}
|
Résultat : |
C:\java>javac -version
javac 15
C:\java>javac TestEnumLocale.java
TestEnumLocale.java:4: error: enum types must not be local
enum Statut { VALIDE, INVALIDE };
^
1 error
C:\java>
|
Java 16 permet de définir des énumérations locales, qui ne pourront donc être utilisées que dans la méthode où elles sont définies.
Résultat : |
C:\java>javac -version
javac 16.0.1
C:\java>javac TestEnumLocale.java
C:\java>
|
Les énumérations locales ne peuvent pas capturer les variables non static du contexte englobant comme les paramètres de la méthode par exemple.
Exemple ( code Java 16 ) : |
public class TestEnumLocale {
public void traiter(int valeur) {
enum Statut {
VALIDE(valeur), INVALIDE(valeur+1);
private final int v;
Statut(int v) {
this.v = v;
}
};
}
}
|
Résultat : |
C:\java>javac TestEnumLocale.java
TestEnumLocale.java:5: error: non-static variable valeur cannot be referenced from a static
context
VALIDE(valeur), INVALIDE(valeur+1);
^
TestEnumLocale.java:5: error: non-static variable valeur cannot be referenced from a static
context
VALIDE(valeur), INVALIDE(valeur+1);
^
2 errors
C:\java>
|
Les énumérations locales peuvent capturer les variables static du contexte englobant.
Exemple ( code Java 16 ) : |
public class TestEnumLocale {
static int valeur = 10;
public void traiter() {
enum Statut {
VALIDE(valeur), INVALIDE(valeur+1);
private final int v;
Statut(int v) {
this.v = v;
}
};
}
}
|
|