Cours Complet sur les Énumérations, Annotations,
Réflexion, Types Primitifs et Autoboxing, et Génériques
en Java
Ce cours a pour but de vous fournir une compréhension complète des concepts fondamentaux en
Java, tels que les énumérations, les annotations, la réflexion, les types primitifs et leur relation
avec leurs wrappers, l'autoboxing et l'unboxing, ainsi que les génériques. Chaque section est
détaillée avec des explications et des exemples pratiques.
1. Les Énumérations (Enumerations)
Introduction aux Énumérations
Les énumérations en Java sont des types spéciaux qui représentent un ensemble fixe de
constantes. Elles permettent de définir des ensembles de valeurs finies, comme les jours de la
semaine, les mois de l'année, etc.
Déclaration d'une Énumération
public enum Jour {
LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE
}
Fonctionnalités des Énumérations
Les énumérations peuvent inclure des constructeurs, des champs et des méthodes. Elles
permettent d'associer des données et un comportement à chaque constante.
Exemple d'Énumération avec Constructeurs, Champs et Méthodes
public enum Saison {
PRINTEMPS("Douceur"),
ETE("Chaleur"),
AUTOMNE("Fraîcheur"),
HIVER("Froid");
private String description;
Saison(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public void afficherMessage() {
System.out.println("La saison est " + this.name() + " et elle est caractérisée par
}
}
public class TestSaison {
public static void main(String[] args) {
Saison saison = Saison.ETE;
saison.afficherMessage();
}
}
Méthodes Utiles dans les Énumérations
• values() : Renvoie un tableau de toutes les constantes de l'énumération.
• valueOf(String name) : Renvoie la constante de l'énumération correspondant au
nom donné.
• ordinal() : Renvoie l'index de la constante dans l'énumération (0 pour la première
constante, 1 pour la seconde, etc.).
Exemple d'utilisation de values() et ordinal()
public class TestEnum {
public static void main(String[] args) {
for (Jour jour : Jour.values()) {
System.out.println(jour + " est le " + jour.ordinal() + "e jour de la semaine.
}
}
}
2. Les Annotations
Introduction aux Annotations
Les annotations en Java sont utilisées pour fournir des métadonnées supplémentaires au code.
Elles peuvent être appliquées aux classes, méthodes, champs, paramètres, et autres éléments
du code. Les annotations sont souvent utilisées par les frameworks pour fournir des informations
au moment de l'exécution.
Annotations Standard en Java
• @Override : Utilisée pour indiquer qu'une méthode redéfinit une méthode dans une
superclasse.
• @Deprecated : Marque un élément comme obsolète, indiquant qu'il ne devrait plus
être utilisé.
• @SuppressWarnings : Indique au compilateur d'ignorer certains avertissements.
Exemple de @Override
class SuperClasse {
void afficher() {
System.out.println("SuperClasse");
}
}
class SousClasse extends SuperClasse {
@Override
void afficher() {
System.out.println("SousClasse");
}
}
Création d'Annotations Personnalisées
Vous pouvez créer vos propres annotations en utilisant la syntaxe suivante :
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // RUNTIME pour que l'annotation soit disponible à l'e
@Target(ElementType.METHOD) // Cible l'annotation pour les méthodes uniquement
public @interface MonAnnotation {
String valeur();
}
Utilisation d'une Annotation Personnalisée
public class MaClasse {
@MonAnnotation(valeur = "Salut")
public void afficherMessage() {
System.out.println("Ceci est un message.");
}
}
Méta-Annotations
Les méta-annotations sont des annotations utilisées pour annoter d'autres annotations. Elles
contrôlent le comportement des annotations.
• @Retention : Indique combien de temps l'annotation doit être conservée.
◦ RetentionPolicy.SOURCE : L'annotation est disponible uniquement
dans le code source et est ignorée par le compilateur.
◦ RetentionPolicy.CLASS : L'annotation est incluse dans le bytecode
mais n'est pas disponible au moment de l'exécution.
◦ RetentionPolicy.RUNTIME : L'annotation est disponible au moment de
l'exécution, ce qui permet d'utiliser la réflexion pour y accéder.
• @Target : Indique les éléments du programme sur lesquels une annotation peut
être appliquée.
◦ ElementType.TYPE : Appliquée aux classes, interfaces, énumérations.
◦ ElementType.FIELD : Appliquée aux champs d'une classe (variables
d'instance).
◦ ElementType.METHOD : Appliquée aux méthodes.
◦ ElementType.PARAMETER : Appliquée aux paramètres de méthode.
◦ ElementType.CONSTRUCTOR : Appliquée aux constructeurs.
◦ ElementType.LOCAL_VARIABLE : Appliquée aux variables locales.
◦ ElementType.ANNOTATION_TYPE : Appliquée aux autres annotations.
◦ ElementType.PACKAGE : Appliquée aux déclarations de package.
3. Réflexion en Java
Introduction à la Réflexion
La réflexion en Java est une API puissante qui permet d'inspecter et de manipuler les classes,
interfaces, champs et méthodes au moment de l'exécution. La réflexion permet notamment de :
• Créer des instances de classes dynamiquement.
• Appeler des méthodes.
• Accéder à des champs privés.
• Gérer des annotations au moment de l'exécution.
Utilisation de la Réflexion avec les Annotations
L'API de réflexion permet d'inspecter les annotations présentes sur les classes, méthodes,
champs, etc., et d'adapter le comportement du programme en fonction de ces annotations.
Exemple Complet : Annotations et Réflexion
Étape 1 : Création d'une Annotation Personnalisée
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MonAnnotation {
String valeur();
}
Étape 2 : Annotation d'une Méthode avec @MonAnnotation
public class MaClasse {
@MonAnnotation(valeur = "Salut")
public void afficherMessage() {
System.out.println("Ceci est un message.");
}
}
Étape 3 : Utilisation de la Réflexion pour Lire et Réagir aux Annotations
import java.lang.reflect.Method;
public class TestReflection {
public static void main(String[] args) {
try {
// Obtenir la classe MaClasse
Class<MaClasse> classe = MaClasse.class;
// Parcourir toutes les méthodes de la classe
for (Method methode : classe.getDeclaredMethods()) {
// Vérifier si la méthode est annotée avec @MonAnnotation
if (methode.isAnnotationPresent(MonAnnotation.class)) {
// Récupérer l'annotation
MonAnnotation annotation = methode.getAnnotation(MonAnnotation.class);
// Afficher la valeur de l'annotation
System.out.println("Valeur de l'annotation : " + annotation.valeur());
// Appeler la méthode annotée
methode.invoke(classe.getDeclaredConstructor().newInstance());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Explication du Code
1. Class<MaClasse> classe = MaClasse.class; : Récupère la classe MaClasse
via l'API de réflexion.
2. for (Method methode : classe.getDeclaredMethods()) : Parcourt toutes les
méthodes déclarées dans la classe MaClasse .
3. if (methode.isAnnotationPresent(MonAnnotation.class)) : Vérifie si une
méthode est annotée avec @MonAnnotation .
4. MonAnnotation annotation = methode.getAnnotation(MonAnnotation.class);
: Récupère l'annotation de la méthode.
5. methode.invoke(classe.getDeclaredConstructor().newInstance()); : Invoque
la méthode annotée à l'aide de l'API de réflexion.
Applications Pratiques
La combinaison des annotations et de la réflexion permet de créer des frameworks et des
bibliothèques robustes où le comportement peut être injecté ou modifié en fonction des
annotations au moment de l'exécution. Par exemple :
• Frameworks de tests unitaires : Les frameworks comme JUnit utilisent la
réflexion pour découvrir les méthodes de test annotées par @Test et les exécuter
• Injection de dépendances : Des frameworks comme Spring utilisent des
annotations telles que @Autowired pour injecter automatiquement les dépendances
dans les classes.
• Sérialisation/Désérialisation : Les bibliothèques comme Jackson ou Gson
utilisent la réflexion pour mapper les objets Java aux structures JSON, XML, etc., en
se basant sur les annotations.
4. Types Primitifs et Types Wrapper en Java
Introduction aux Types Primitifs
Java propose huit types de données primitifs qui sont utilisés pour définir les données de base.
Ces types sont plus légers et plus rapides que les objets, car ils ne nécessitent pas d'overhead
de gestion d'objets.
Les types primitifs en Java sont :
• byte : 8 bits, entier signé.
• short : 16 bits, entier signé.
• int : 32 bits, entier signé.
• long : 64 bits, entier signé.
• float : 32 bits, nombre à virgule flottante.
• double : 64 bits, nombre à virgule flottante double précision.
• char : 16 bits, un caractère Unicode.
• boolean : Représente une valeur vraie ou fausse.
Les Types Wrapper
Pour chaque type primitif, Java fournit une classe wrapper correspondante dans le package
java.lang . Ces classes sont utilisées pour encapsuler une valeur primitive dans un objet. Elles
permettent de traiter les types primitifs comme des objets, ce qui est utile dans de nombreuses
situations, notamment avec les collections génériques.
Voici les classes wrapper pour chaque type primitif :
• byte : Byte
• short : Short
• int : Integer
• long : Long
• float : Float
• double : Double
• char : Character
• boolean : Boolean
Exemple d'utilisation des Types Wrapper
public class ExempleWrapper {
public static void main(String[] args) {
int nombrePrimitif = 10;
Integer nombreObjet = Integer.valueOf(nombrePrimitif); // Conversion manuelle (box
int nombreRetourne = nombreObjet.intValue(); // Conversion manuelle (unboxing)
System.out.println("Nombre retourné : " + nombreRetourne);
}
}
Pourquoi utiliser les Types Wrapper ?
1. Collections : Les collections en Java ne peuvent contenir que des objets. Si vous
souhaitez stocker des valeurs primitives dans une collection, vous devez les
encapsuler dans leurs classes wrapper.
2. Méthodes Génériques : Les méthodes qui fonctionnent avec des objets
génériques nécessitent souvent des types objets, pas des types primitifs.
3. Utilisation des Méthodes Utilitaires : Les classes wrapper fournissent des
méthodes utilitaires comme parseInt , toString , et bien d'autres qui ne sont pas
disponibles avec les types primitifs.
5. Autoboxing et Unboxing
Introduction à l'Autoboxing et Unboxing
L'autoboxing et l'unboxing sont des fonctionnalités introduites à partir de Java 5 qui permettent la
conversion automatique entre les types primitifs et leurs classes wrapper correspondantes.
• Autoboxing : Conversion automatique d'un type primitif en son équivalent wrapper.
• Unboxing : Conversion automatique d'un objet wrapper en son équivalent primitif.
Exemples d'Autoboxing et Unboxing
Autoboxing
public class ExempleAutoboxing {
public static void main(String[] args) {
int nombrePrimitif = 5;
Integer nombreObjet = nombrePrimitif; // Autoboxing automatique
System.out.println("Nombre objet : " + nombreObjet);
}
}
Unboxing
public class ExempleUnboxing {
public static void main(String[] args) {
Integer nombreObjet = 10;
int nombrePrimitif = nombreObjet; // Unboxing automatique
System.out.println("Nombre primitif : " + nombrePrimitif);
}
}
Avantages de l'Autoboxing et Unboxing
• Simplicité : Simplifie le code en éliminant la nécessité de conversions manuelles
entre les types primitifs et leurs classes wrapper.
• Interopérabilité avec les Collections : Facilite l'utilisation des types primitifs avec
les collections génériques, comme List , Set , etc.
Limitations
• Performance : L'utilisation excessive de l'autoboxing peut affecter la performance
de l'application, car elle nécessite la création d'objets supplémentaires.
• NullPointerException : Lors de l'unboxing, si l'objet wrapper est null , une
NullPointerException sera levée.
6. Les Génériques (Generics)
Introduction aux Génériques
Les génériques permettent d'écrire des classes, des interfaces et des méthodes qui peuvent
fonctionner avec n'importe quel type de données tout en offrant une sécurité de type au moment
de la compilation. Les génériques sont un puissant outil pour la réutilisation du code en Java.
Syntaxe des Génériques
public class Boite<T> {
private T contenu;
public void setContenu(T contenu) {
this.contenu = contenu;
}
public T getContenu() {
return contenu;
}
}
Utilisation des Génériques
public class TestGenerics {
public static void main(String[] args) {
Boite<String> boiteDeTexte = new Boite<>();
boiteDeTexte.setContenu("Bonjour");
System.out.println("Contenu : " + boiteDeTexte.getContenu());
Boite<Integer> boiteDeNombre = new Boite<>();
boiteDeNombre.setContenu(123);
System.out.println("Contenu : " + boiteDeNombre.getContenu());
}
}
Génériques avec Borne Supérieure ( extends )
Vous pouvez restreindre les types acceptés par une classe ou une méthode générique en
utilisant une borne supérieure ( extends ).
public class Calculateur<T extends Number> {
private T valeur;
public Calculateur(T valeur) {
this.valeur = valeur;
}
public double doubler() {
return valeur.doubleValue() * 2;
}
}
Wildcards ( ? )
Les wildcards sont utilisés pour représenter un type inconnu. Elles sont souvent utilisées avec
des génériques pour écrire des méthodes qui fonctionnent avec n'importe quel type générique.
Wildcard avec Borne Supérieure ( ? extends )
Cette wildcard indique que le type peut être de n'importe quel type qui est une sous-classe de la
classe spécifiée.
public static double somme(List<? extends Number> liste) {
double somme = 0.0;
for (Number n : liste) {
somme += n.doubleValue();
}
return somme;
}
Wildcard avec Borne Inférieure ( ? super )
Cette wildcard indique que le type peut être de n'importe quel type qui est une super-classe de la
classe spécifiée.
public static void ajouterNombre(List<? super Integer> liste) {
liste.add(5);
}
Types Génériques dans les Méthodes
Les méthodes génériques sont des méthodes qui déclarent leur propre type générique. Cela les
rend flexibles et réutilisables pour différents types de données.
public class Utilitaire {
public static <T> void afficherTableau(T[] tableau) {
for (T element : tableau) {
System.out.println(element);
}
}
public static void main(String[] args) {
String[] tableauDeString = {"A", "B", "C"};
Integer[] tableauDeNombre = {1, 2, 3};
afficherTableau(tableauDeString);
afficherTableau(tableauDeNombre);
}
}
Génériques et Collections
Les collections en Java utilisent largement les génériques. Par exemple, une List<String> est
une liste qui ne peut contenir que des objets String .
import java.util.ArrayList;
import java.util.List;
public class ExempleCollectionGenerique {
public static void main(String[] args) {
List<String> listeDeString = new ArrayList<>();
listeDeString.add("Bonjour");
listeDeString.add("Monde");
for (String str : listeDeString) {
System.out.println(str);
}
}
}
Limitations des Génériques
1. Pas de Types Primitifs : Les génériques ne peuvent pas être utilisés avec des
types primitifs comme int , char , etc.
2. Pas de Création d'Instances de Type Générique : Vous ne pouvez pas créer
directement une instance d'un type générique.
// Ce code ne fonctionnera pas
T objet = new T(); // Erreur
3. Erasure des Types : Les informations de type sont effacées au moment de
l'exécution, ce qui signifie que le type générique n'est pas connu à l'exécution.
Bonnes Pratiques avec les Génériques
1. Utilisez les génériques pour renforcer la sécurité de type : Évitez les casts
dangereux et laissez le compilateur gérer les types.
2. **Utilisez les wildcards à bon escient
** : Les wildcards permettent d'écrire du code plus flexible tout en maintenant la sécurité de type.
3. Comprenez les bornes ( extends et super ) : Les bornes permettent de restreindre ou
d'élargir les types acceptés par les génériques.
Ce cours couvre en détail les concepts des énumérations, des annotations, de la réflexion, des
types primitifs et de leurs wrappers, de l'autoboxing et unboxing, et des génériques en Java. Ces
notions sont essentielles pour écrire un code Java plus propre, flexible et maintenable, ainsi que
pour comprendre les frameworks Java modernes. N'hésitez pas à expérimenter et à appliquer
ces concepts dans divers projets pour renforcer votre compréhension.