Comment et quand utiliser Enums et Annotations dans Java
Sommaire
1. Introduction
Dans cette partie du didacticiel, nous allons couvrir deux fonctionnalités intéressantes introduites dans le langage Java : les énumérations et les annotations. Les énumérations peuvent être traitées comme un type spécial de classes et les annotations comme un type spécial d’interfaces.
L’idée des énumérations est simple, mais très pratique: elle représente un ensemble fixe et constant de valeurs. Cela signifie en pratique que les enums sont souvent utilisés pour concevoir des concepts qui ont un ensemble constant d’états possibles. Par exemple, les jours de la semaine sont un excellent exemple des enums: ils sont limités aux lundi, mardi, mercredi, jeudi, vendredi, samedi et dimanche.
De l’autre côté, les annotations sont un type spécial de métadonnées pouvant être associées à différents éléments et constructions du langage Java. Les annotations ont beaucoup contribué à l’élimination des descripteurs XML standard utilisés dans Java, presque partout. Ils ont présenté le nouveau moyen robuste et sûr de configuration et de personnalisation.
2. Enums en tant que classes spéciales
Avant que les enums aient été introduits dans le langage Java, la manière habituelle de modéliser l’ensemble de valeurs fixes en Java consistait simplement à déclarer un certain nombre de constantes. Par exemple:
public class DaysOfTheWeekConstants {
public static final int MONDAY = 0;
public static final int TUESDAY = 1;
public static final int WEDNESDAY = 2;
public static final int THURSDAY = 3;
public static final int FRIDAY = 4;
public static final int SATURDAY = 5;
public static final int SUNDAY = 6;
}
Bien que cette approche fonctionne, elle est loin d’être la solution idéale. Principalement parce que les constantes elles-mêmes ne sont que des valeurs de type int et que chaque endroit du code où ces constantes sont attendues devrait être explicitement documenté et affirmé tout le temps.
Sémantiquement, ce n’est pas une représentation du concept sûre comme le montre la méthode suivante.
public boolean isWeekend( int day ) {
return( day == SATURDAY || day == SUNDAY );
}
D’un point de vue logique, l’argument day
devrait avoir l’une des valeurs déclarées dans la classe DaysOfTheWeekConstants
. Cependant, il n’est pas possible de le deviner sans documentation supplémentaire en cours d’écriture (et lue ultérieurement par quelqu’un). Pour le compilateur Java, l’appel de isWeekend(100)
semble tout à fait correct et ne pose aucun problème.
Ici, les enums
viennent à la rescousse. Les énumérations permettent de remplacer les constantes par les valeurs typées et d’utiliser ces types partout. Laissez-nous réécrire la solution ci-dessus en utilisant des énumérations.
public enum DaysOfTheWeek {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
Ce qui a changé, c’est que la classe devient enum et que les valeurs possibles sont listées dans la définition enum. Cependant, la différence réside dans le fait que chaque valeur est l’instance de la classe de enum à laquelle elle est déclarée (dans notre exemple, DaysOfTheWeek). En tant que tel, chaque fois que des énumérations sont utilisées, le compilateur Java peut effectuer une vérification de type. Par exemple:
public boolean isWeekend( DaysOfTheWeek day ) {
return( day == SATURDAY || day == SUNDAY );
}
Veuillez noter que l’utilisation du schéma de nommage en majuscules dans enums n’est qu’une convention, rien ne vous empêche vraiment de ne pas le faire.
3. Enums et champs d’instance
Les énumérations sont des classes spécialisées et sont donc extensibles. Cela signifie qu’ils peuvent avoir des champs d’instance, des constructeurs et des méthodes (bien que les seules limitations soient que le constructeur no-args par défaut ne peut pas être déclaré et que tous les constructeurs doivent être private
). Ajoutons la propriété isWeekend
à chaque jour de la semaine en utilisant le champ d’instance et le constructeur.
public enum DaysOfTheWeekFields {
MONDAY( false ),
TUESDAY( false ),
WEDNESDAY( false ),
THURSDAY( false ),
FRIDAY( false ),
SATURDAY( true ),
SUNDAY( true );
private final boolean isWeekend;
private DaysOfTheWeekFields( final boolean isWeekend ) {
this.isWeekend = isWeekend;
}
public boolean isWeekend() {
return isWeekend;
}
}
Comme nous pouvons le constater, les valeurs des énumérations ne sont que des appels de constructeur, avec la simplification que le nouveau mot clé n’est pas requis. La propriété isWeekend()
peut être utilisée pour détecter si la valeur représente le jour de la semaine ou le week-end. Par exemple:
public boolean isWeekend( DaysOfTheWeek day ) {
return day.isWeekend();
}
Les champs d’instance sont une fonctionnalité extrêmement utile des enums en Java. Ils sont très souvent utilisés pour associer des détails supplémentaires à chaque valeur, en utilisant des règles de déclaration de classe normales.
4. Enums et interfaces
Une autre caractéristique intéressante, qui confirme encore une fois que les énumérations ne sont que des classes spécialisées, est qu’elles peuvent implémenter des interfaces (cependant, les énumérations ne peuvent pas étendre d’autres classes pour les raisons expliquées plus loin dans la section Énergies et génériques). Par exemple, introduisons l’interface DayOfWeek
.
interface DayOfWeek {
boolean isWeekend();
}
Et réécrivez l’exemple de la section précédente en utilisant une implémentation d’interface plutôt que des champs d’instance réguliers.
public enum DaysOfTheWeekInterfaces implements DayOfWeek {
MONDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
TUESDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
WEDNESDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
THURSDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
FRIDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
SATURDAY() {
@Override
public boolean isWeekend() {
return true;
}
},
SUNDAY() {
@Override
public boolean isWeekend() {
return true;
}
};
}
La manière dont nous avons implémenté l’interface est un peu prolixe, mais il est certainement possible de l’améliorer en combinant des champs d’instance et des interfaces. Par exemple:
public enum DaysOfTheWeekFieldsInterfaces implements DayOfWeek {
MONDAY( false ),
TUESDAY( false ),
WEDNESDAY( false ),
THURSDAY( false ),
FRIDAY( false ),
SATURDAY( true ),
SUNDAY( true );
private final boolean isWeekend;
private DaysOfTheWeekFieldsInterfaces( final boolean isWeekend ) {
this.isWeekend = isWeekend;
}
@Override
public boolean isWeekend() {
return isWeekend;
}
}
En prenant en charge les champs d’instance et les interfaces, les énumérations peuvent être utilisées de manière plus orientée objet, ce qui permet de s’appuyer sur un certain niveau d’abstraction.
5. Enums et classe générique
Bien qu’il ne soit pas visible au premier coup d’œil, il existe une relation entre enums
et les classes génériques en Java. Chaque énumération unique en Java est automatiquement héritée de la classe générique Enum <T>
, où T
est le type enum lui-même. Le compilateur Java effectue cette transformation pour le compte du développeur lors de la compilation, en développant la déclaration enum public enum DaysOfTheWeek
à quelque chose comme ceci:
public class DaysOfTheWeek extends Enum< DaysOfTheWeek > {
// Other declarations here
}
Cela explique également pourquoi les enums peuvent implémenter des interfaces mais ne peuvent pas étendre d’autres classes: ils étendent implicitement Enum <T>
et, comme nous le savons dans la deuxième partie du tutoriel, à l’aide de méthodes communes à tous les objets, Java ne prend pas en charge l’héritage multiple.
Le fait que chaque énumération se prolonge Enum <T>
permet de définir des classes génériques, des interfaces et des méthodes qui attendent les instances de types enum en tant qu’arguments ou paramètres de type. Par exemple:
public< T extends Enum < ? > > void performAction( final T instance ) {
// Perform some action here
}
Dans la déclaration de méthode ci-dessus, le type T
est contraint d’être l’instance de tout enum et le compilateur Java le vérifiera.
6. Méthodes d’énumération
La classe de base Enum <T>
fournit quelques méthodes utiles, qui sont automatiquement héritées par chaque instance enum.
Méthode | Déscription |
String name() |
Retourne le nom de cette constante enum, exactement comme indiqué dans sa déclaration enum. |
int ordinal() |
Renvoie l’ordinal de cette constante d’énumération (sa position dans sa déclaration enum, où la constante initiale se voit attribuer un ordinal égal à zéro). |
De plus, le compilateur Java génère automatiquement deux méthodes statiques supplémentaires utiles pour chaque type d’énum qu’il rencontre (appelons-le T comme type particulier d’énum).
Méthode | Description |
T[] values() |
Renvoie toutes les constantes d’énumération déclarées pour l’énumération T. |
T valueOf(String name) |
Renvoie la constante d’énumération T avec le nom spécifié. |
En raison de la présence de ces méthodes et du travail acharné du compilateur, l’utilisation d’énums dans votre code présente un avantage supplémentaire: ils peuvent être utilisés dans des instructions switch / case
. Par exemple:
public void performAction( DaysOfTheWeek instance ) {
switch( instance ) {
case MONDAY:
// Do something
break;
case TUESDAY:
// Do something
break;
// Other enum constants here
}
}
7. Collections spécialisées: EnumSet et EnumMap
Des instances d’énumérations, comme toutes les autres classes, peuvent être utilisées avec la bibliothèque de collection Java standard. Cependant, certains types de collection ont été optimisés spécifiquement pour les énumérations et il est recommandé dans la plupart des cas d’être utilisés à la place d’homologues à usage général.
types de collection spécialisés: EnumSet <T>
et EnumMap <T,? >
. Les deux sont très faciles à utiliser et nous allons commencer par l’EnumSet <T>
.
EnumSet <T>
est l’ensemble standard optimisé pour stocker efficacement des énumérations. EnumSet <T>
ne peut pas être instancié à l’aide des constructeurs et fournit à la place beaucoup de méthodes utiles .
Par exemple, la méthode allOf
crée l’instance de EnumSet <T>
contenant toutes les constantes enum du type enum en question:
final Set< DaysOfTheWeek > enumSetAll = EnumSet.allOf( DaysOfTheWeek.class );
Par conséquent, la méthode noneOf
crée l’instance d’un EnumSet <T>
vide pour le type enum en question:
final Set< DaysOfTheWeek > enumSetNone = EnumSet.noneOf( DaysOfTheWeek.class );
Il est également possible de spécifier quelles constantes enum du type enum en question doivent être incluses dans EnumSet <T>
, en utilisant la méthode factory:
final Set< DaysOfTheWeek > enumSetSome = EnumSet.of(
DaysOfTheWeek.SUNDAY,
DaysOfTheWeek.SATURDAY
);
Le Map Enum <T,? >
est très proche de Map normale, à la différence que ses clés pourraient être les constantes d’énum du type d’énum en question. Par exemple:
final Map< DaysOfTheWeek, String > enumMap = new EnumMap<>( DaysOfTheWeek.class );
enumMap.put( DaysOfTheWeek.MONDAY, "Lundi" );
enumMap.put( DaysOfTheWeek.TUESDAY, "Mardi" );
Veuillez noter que, comme la plupart des implémentations de collection, EnumSet <T> et EnumMap <T,? > ne sont pas thread-safe et ne peuvent pas être utilisés tels quels dans un environnement multithread .
8. Quand utiliser les enums
Depuis Java 5, les énumérations constituent le seul moyen préféré et recommandé de représenter le jeu de constantes. Non seulement ils sont fortement typés, ils sont extensibles et pris en charge par toute bibliothèque ou tout framework moderne.
9. Annotations et interfaces spéciales
Comme nous l’avons mentionné précédemment, les annotations sont utilisé pour associer les métadonnées à différents éléments du langage Java.
Les annotations en elles-mêmes n’ont aucun effet direct sur l’élément qu’elles annotent. Cependant, en fonction des annotations et de la manière dont elles sont définies, elles peuvent être utilisées par le compilateur Java (le bon exemple en est l’annotation @Override
), par les processeurs d’annotation et par le code à l’exécution utilisant des techniques de réflexion et autres techniques d’introspection .
Jetons un coup d’œil à la déclaration d’annotation la plus simple possible:
public @interface SimpleAnnotation {
}
The @interface keyword introduces new annotation type. That is why annotations could be treated as specialized interfaces. Annotations may declare the attributes with or without default values, for example:
public @interface SimpleAnnotationWithAttributes {
String name();
int order() default 0;
}
Si une annotation déclare un attribut sans valeur par défaut, il doit être fourni à tous les endroits où l’annotation est appliquée. Par exemple:
@SimpleAnnotationWithAttributes( name = "new annotation" )
Par convention, si l’annotation a un attribut avec la valeur name
et que c’est le seul qui doit être spécifié, le nom de l’attribut peut être omis, par exemple:
public @interface SimpleAnnotationWithValue {
String value();
}
peut être utilisé de la façon suivante:
@SimpleAnnotationWithValue( "new annotation" )
Il existe quelques limitations qui, dans certains cas d’utilisation, rendent le travail avec des annotations peu pratique. Premièrement, les annotations ne supportent aucun type d’héritage: une annotation ne peut pas étendre une autre annotation.
Deuxièmement, il n’est pas possible de créer une instance d’annotation à l’aide d’un programme par le nouvel opérateur.
Et troisièmement, les annotations ne peuvent déclarer que des attributs de types primitifs, String, Class et tableaux de ceux-ci. Aucune méthode ou constructeur n’est autorisé à être déclaré dans les annotations.
10. Politique d’annotation et de rétention
Chaque annotation possède la caractéristique très importante appelée stratégie de rétention, qui est une énumération (de type RetentionPolicy
) avec l’ensemble des stratégies permettant de conserver les annotations. Il pourrait être défini sur l’une des valeurs suivantes.
Policy | Description |
CLASS |
Les annotations doivent être enregistrées dans le fichier de classe par le compilateur mais ne doivent pas être conservées par la machine virtuelle au moment de l’exécution. |
RUNTIME |
Les annotations doivent être enregistrées dans le fichier de classe par le compilateur et conservées par la machine virtuelle au moment de l’exécution afin qu’elles puissent être lues de manière réfléchie. |
SOURCE |
Les annotations doivent être ignorées par le compilateur. |
La stratégie de rétention a un effet crucial sur le moment où l’annotation sera disponible pour le traitement. La stratégie de rétention peut être définie à l’aide de l’annotation @Retention
. Par exemple:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention( RetentionPolicy.RUNTIME )
public @interface AnnotationWithRetention {
}
Définir la stratégie de conservation des annotations sur RUNTIME garantira sa présence dans le processus de compilation et dans l’application en cours d’exécution.
11. Annotations et types d’éléments
Une autre caractéristique que doit avoir chaque annotation est le type d’élément auquel elle pourrait être appliquée. De la même manière que la stratégie de rétention, elle est définie comme une énumération (ElementType) avec l’ensemble des types d’éléments possibles.
Type d’élément | Description |
ANNOTATION_TYPE |
Déclaration de type d’annotation |
CONSTRUCTOR |
Déclaration du constructeur |
FIELD |
Déclaration de champ (inclut les constantes enum) |
LOCAL_VARIABLE |
Déclaration de variable locale |
METHOD |
Déclaration de méthode |
PACKAGE |
Déclaration de package |
PARAMETER |
Déclaration des paramètres |
TYPE |
Classe, interface (y compris le type d’annotation) ou déclaration d’enum |
En plus de ceux décrits ci-dessus, Java 8 introduit deux nouveaux types d’éléments auxquels les annotations peuvent être appliquées.
Type d’élément | Description |
TYPE_PARAMETER |
Déclaration de paramètre de type |
TYPE_USE |
Utilisation d’un type |
Contrairement aux règles de rétention, une annotation peut déclarer plusieurs types d’élément auxquels elle peut être associée, à l’aide de l’annotation @Target
. Par exemple:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target( { ElementType.FIELD, ElementType.METHOD } )
public @interface AnnotationWithTarget {
}
La plupart des annotations que vous allez créer doivent avoir à la fois une stratégie de rétention et des types d’élément spécifiés pour être utiles.
12. Annotations et héritage
La relation importante existe entre la déclaration des annotations et l’héritage en Java. Par défaut, les sous-classes n’héritent pas de l’annotation déclarée sur la classe parente. Cependant, il existe un moyen de propager des annotations particulières dans toute la hiérarchie de classe à l’aide de l’annotation @Inherited
. Par exemple:
@Target( { ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
@Inherited
@interface InheritableAnnotation {
}
@InheritableAnnotation
public class Parent {
}
public class Child extends Parent {
}
Dans cet exemple, l’annotation @InheritableAnnotation
La déclaration déclarée sur la classe Parent sera également héritée par la classe Child.
13. Quand utiliser les annotations
Les annotations sont littéralement partout: la bibliothèque standard Java en contient beaucoup, mais chaque spécification Java inclut également les annotations. Chaque fois que vous devez associer des métadonnées supplémentaires à votre code, les annotations sont un moyen simple et facile de le faire.
Il est intéressant de noter que la communauté Java s’efforce de développer des concepts sémantiques communs et de normaliser les annotations sur plusieurs technologies Java . Pour le moment, les annotations suivantes sont incluses dans la bibliothèque Java standard.
Annotation | La description |
@Deprecated |
Indique que l’élément marqué est obsolète et ne doit plus être utilisé. Le compilateur génère un avertissement chaque fois qu’un programme utilise une méthode, une classe ou un champ avec cette annotation. |
@Override |
Indique au compilateur que l’élément est censé remplacer un élément déclaré dans une superclasse. |
@SuppressWarnings |
Ordonne au compilateur de supprimer les avertissements spécifiques qu’il générerait autrement. |
@SafeVarargs |
Lorsqu’il est appliqué à une méthode ou à un constructeur, affirme que le code n’effectue pas d’opérations potentiellement dangereuses sur son paramètre varargs. Lorsque ce type d’annotation est utilisé, les avertissements non contrôlés relatifs à l’utilisation de varargs sont supprimés (plus de détails sur les varargs seront traités dans la partie 6 du tutoriel,Comment écrire des méthodes efficacement ). |
@Retention |
Spécifie comment l’annotation marquée est conservée. |
@Target |
Spécifie le type d’éléments Java auxquels l’annotation marquée peut être appliquée. |
@Documented |
Indique que chaque fois que l’annotation spécifiée est utilisée, ces éléments doivent être documentés à l’aide de l’outil Javadoc (par défaut, les annotations ne sont pas incluses dans Javadoc). |
@Inherited |
Indique que le type d’annotation peut être hérité de la super classe (pour plus de détails, reportez-vous à la sectionAnnotations et héritage ). |