8.3Génériques
8.3.1 – Principe
Les génériques (ou generics) permettent de créer des classes, interfaces et méthodes qui fonctionnent avec différents types tout en conservant la sécurité de type à la compilation. Ils éliminent le besoin de casting et améliorent la sécurité et la lisibilité du code.
❌ Problème sans génériques
Avant les génériques (Java 5), les collections stockaient des objets de type Object, ce qui nécessitait des casts et pouvait causer des erreurs à l'exécution.
Exemple : Code sans génériques (ancien style)
// Sans génériques : stocke des Object
List liste = new ArrayList();
liste.add("Hello");
liste.add(123); // Pas d'erreur à la compilation !
// Cast nécessaire et risque d'erreur à l'exécution
String s = (String) liste.get(0); // OK
// String s2 = (String) liste.get(1); // ❌ ClassCastException à l'exécution !
✅ Solution avec génériques
Les génériques permettent de spécifier le type d'éléments que la collection contient, garantissant la sécurité de type à la compilation.
Exemple : Code avec génériques
// Avec génériques : spécifie le type
List<String> liste = new ArrayList<>();
liste.add("Hello");
// liste.add(123); // ❌ ERREUR à la compilation ! Type incompatible
// Pas besoin de cast
String s = liste.get(0); // Type garanti : String
🔑 Syntaxe des génériques
Les génériques utilisent des chevrons <> pour spécifier le type. La lettre entre chevrons est un paramètre de type (souvent T, E, K, V).
Exemple : Syntaxe des génériques
// List de String
List<String> chaines = new ArrayList<String>();
// Depuis Java 7, inférence de type (diamond operator)
List<String> chaines = new ArrayList<>();
// List d'Integer
List<Integer> nombres = new ArrayList<>();
// Map avec clé String et valeur Integer
Map<String, Integer> map = new HashMap<>();
💡 Avantages des génériques
- Sécurité de type : Erreurs détectées à la compilation, pas à l'exécution
- Pas de casting : Plus besoin de convertir les types
- Code plus lisible : Le type est explicite dans la déclaration
- Meilleure documentation : Le code documente lui-même les types utilisés
8.3.2 – Utilisation simple
Les génériques sont principalement utilisés avec les collections Java. Voici les cas d'usage les plus courants.
📋 List avec génériques
Exemple : List typée
import java.util.ArrayList;
import java.util.List;
// List de String
List<String> chaines = new ArrayList<>();
chaines.add("Hello");
chaines.add("World");
String premier = chaines.get(0); // Pas de cast nécessaire
// List d'Integer
List<Integer> nombres = new ArrayList<>();
nombres.add(10);
nombres.add(20);
int somme = nombres.get(0) + nombres.get(1); // Type garanti
🔢 Set avec génériques
Exemple : Set typé
import java.util.HashSet;
import java.util.Set;
Set<String> uniques = new HashSet<>();
uniques.add("Alice");
uniques.add("Bob");
// uniques.add(123); // ❌ ERREUR : type incompatible
🗺️ Map avec génériques
Les Map utilisent deux paramètres de type : un pour la clé (K) et un pour la valeur (V).
Exemple : Map typée
import java.util.HashMap;
import java.util.Map;
// Map avec clé String et valeur Integer
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 25);
ages.put("Bob", 30);
Integer ageAlice = ages.get("Alice"); // Type garanti : Integer
// Map avec clé Integer et valeur String
Map<Integer, String> codes = new HashMap<>();
codes.put(1, "Un");
codes.put(2, "Deux");
String valeur = codes.get(1); // Type garanti : String
🔄 Collections de vos propres objets
Exemple : Collection d'objets personnalisés
public class Personne {
private String nom;
private int age;
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
// Getters...
}
// List de Personne
List<Personne> personnes = new ArrayList<>();
personnes.add(new Personne("Alice", 25));
personnes.add(new Personne("Bob", 30));
// Parcours typé
for (Personne p : personnes) {
System.out.println(p.getNom()); // Pas de cast nécessaire
}
⚠️ Raw Types (types bruts)
Vous pouvez toujours utiliser les collections sans génériques (raw types), mais c'est déconseillé car vous perdez la sécurité de type.
Exemple : Raw types (à éviter)
// Raw type : pas de générique
List liste = new ArrayList(); // ⚠️ Déconseillé
liste.add("Hello");
liste.add(123); // Pas d'erreur, mais dangereux
// Cast nécessaire et risque d'erreur
String s = (String) liste.get(0);
💡 Points clés à retenir
- Génériques : Spécifient le type d'éléments dans les collections
- Syntaxe :
List<String>,Map<K, V> - Diamond operator :
new ArrayList<>()(Java 7+) - Sécurité : Erreurs détectées à la compilation
- Pas de cast : Plus besoin de convertir les types
- Toujours utiliser : Préférez toujours les génériques aux raw types
- Toujours spécifier le type avec les génériques pour la sécurité de type
- Utilisez le diamond operator
<>depuis Java 7 - Les génériques fonctionnent avec toutes les collections (List, Set, Map)
- Vous pouvez créer vos propres classes génériques
- Évitez les raw types sauf pour la compatibilité avec d'ancien code
8.3.3 – Création de classes génériques
Vous pouvez créer vos propres classes génériques en déclarant un ou plusieurs paramètres de type dans la déclaration de la classe. Cela permet de créer des classes réutilisables qui fonctionnent avec différents types tout en conservant la sécurité de type.
📝 Structure de base
Pour créer une classe générique, vous déclarez un paramètre de type entre chevrons après le nom de la classe. Par convention, on utilise des lettres majuscules :
T: Type (Type)E: Élément (Element)K: Clé (Key)V: Valeur (Value)N: Nombre (Number)
💻 Exemple 1 : Classe générique simple
Exemple : Boîte générique
// Classe générique avec un paramètre de type T
public class Boite<T> {
private T contenu;
public Boite(T contenu) {
this.contenu = contenu;
}
public T getContenu() {
return contenu;
}
public void setContenu(T contenu) {
this.contenu = contenu;
}
public void afficher() {
System.out.println("Contenu : " + contenu);
}
}
// Utilisation
public class TestBoite {
public static void main(String[] args) {
// Boîte de String
Boite<String> boiteString = new Boite<>("Hello");
String texte = boiteString.getContenu(); // Type garanti : String
// Boîte d'Integer
Boite<Integer> boiteInt = new Boite<>(42);
int nombre = boiteInt.getContenu(); // Type garanti : Integer
// Boîte de Personne
Boite<Personne> boitePersonne = new Boite<>(new Personne("Alice", 25));
Personne p = boitePersonne.getContenu(); // Type garanti : Personne
}
}
<T>: Déclare un paramètre de type nommé Tprivate T contenu;: Utilise T comme type de l'attributpublic T getContenu(): Retourne un objet de type T- À l'utilisation, T est remplacé par le type réel (String, Integer, Personne, etc.)
💻 Exemple 2 : Classe générique avec plusieurs paramètres
Exemple : Paire générique
// Classe générique avec deux paramètres de type
public class Paire<T, U> {
private T premier;
private U second;
public Paire(T premier, U second) {
this.premier = premier;
this.second = second;
}
public T getPremier() {
return premier;
}
public U getSecond() {
return second;
}
public void setPremier(T premier) {
this.premier = premier;
}
public void setSecond(U second) {
this.second = second;
}
@Override
public String toString() {
return "Paire{" + premier + ", " + second + "}";
}
}
// Utilisation
public class TestPaire {
public static void main(String[] args) {
// Paire String-Integer
Paire<String, Integer> paire1 = new Paire<>("Alice", 25);
String nom = paire1.getPremier(); // Type garanti : String
Integer age = paire1.getSecond(); // Type garanti : Integer
// Paire Integer-String
Paire<Integer, String> paire2 = new Paire<>(1, "Un");
// Paire Personne-String
Paire<Personne, String> paire3 = new Paire<>(
new Personne("Bob", 30),
"Développeur"
);
}
}
💻 Exemple 3 : Pile générique (Stack)
Exemple : Implémentation d'une pile générique
import java.util.ArrayList;
import java.util.List;
public class Pile<T> {
private List<T> elements;
public Pile() {
this.elements = new ArrayList<>();
}
// Empiler un élément
public void empiler(T element) {
elements.add(element);
}
// Dépiler un élément
public T depiler() {
if (elements.isEmpty()) {
throw new RuntimeException("La pile est vide");
}
return elements.remove(elements.size() - 1);
}
// Voir le sommet sans dépiler
public T sommet() {
if (elements.isEmpty()) {
throw new RuntimeException("La pile est vide");
}
return elements.get(elements.size() - 1);
}
public boolean estVide() {
return elements.isEmpty();
}
public int taille() {
return elements.size();
}
}
// Utilisation
public class TestPile {
public static void main(String[] args) {
// Pile de String
Pile<String> pileString = new Pile<>();
pileString.empiler("Premier");
pileString.empiler("Deuxième");
pileString.empiler("Troisième");
while (!pileString.estVide()) {
System.out.println(pileString.depiler()); // Type garanti : String
}
// Pile d'Integer
Pile<Integer> pileInt = new Pile<>();
pileInt.empiler(10);
pileInt.empiler(20);
int valeur = pileInt.depiler(); // Type garanti : Integer
}
}
🔒 Contraintes de type (Bounded Type Parameters)
Vous pouvez contraindre le paramètre de type à être un sous-type d'une classe ou interface spécifique en utilisant le mot-clé extends.
Exemple : Classe avec contrainte
// T doit être un sous-type de Number
public class Calculatrice<T extends Number> {
private T valeur1;
private T valeur2;
public Calculatrice(T valeur1, T valeur2) {
this.valeur1 = valeur1;
this.valeur2 = valeur2;
}
public double additionner() {
return valeur1.doubleValue() + valeur2.doubleValue();
}
public double multiplier() {
return valeur1.doubleValue() * valeur2.doubleValue();
}
}
// Utilisation
Calculatrice<Integer> calc1 = new Calculatrice<>(10, 20); // ✅ OK
Calculatrice<Double> calc2 = new Calculatrice<>(10.5, 20.5); // ✅ OK
// Calculatrice<String> calc3 = new Calculatrice<>("a", "b"); // ❌ ERREUR : String n'étend pas Number
Exemple : Contrainte avec interface
// T doit implémenter Comparable
public class Tri<T extends Comparable<T>> {
public T maximum(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// Utilisation
Tri<String> triString = new Tri<>();
String max = triString.maximum("Alice", "Bob"); // ✅ OK : String implémente Comparable
Tri<Integer> triInt = new Tri<>();
Integer maxInt = triInt.maximum(10, 20); // ✅ OK : Integer implémente Comparable
Exemple : Contrainte multiple
// T doit être un sous-type de Number ET implémenter Comparable
public class Comparaison<T extends Number & Comparable<T>> {
public int comparer(T a, T b) {
return a.compareTo(b);
}
}
// Utilisation
Comparaison<Integer> comp = new Comparaison<>();
int resultat = comp.comparer(10, 20); // ✅ OK : Integer étend Number et implémente Comparable
- Vous pouvez avoir une seule classe dans les contraintes (avec
extends) - Vous pouvez avoir plusieurs interfaces (séparées par
&) - La classe doit être mentionnée en premier, puis les interfaces
- Syntaxe :
<T extends Classe & Interface1 & Interface2>
💡 Points clés à retenir
- Déclaration :
public class MaClasse<T> - Plusieurs paramètres :
public class Paire<T, U> - Convention : Utiliser T, E, K, V pour les paramètres de type
- Contraintes :
<T extends Classe>ou<T extends Interface> - Utilisation :
MaClasse<String> obj = new MaClasse<>(); - Sécurité de type : Le type est vérifié à la compilation
8.3.4 – Héritage avec les génériques
L'héritage avec les génériques peut être complexe. Il est important de comprendre la relation entre les classes génériques et leurs sous-classes, ainsi que les règles de compatibilité.
🔗 Héritage de classes génériques
Une classe peut étendre une classe générique en spécifiant le type ou en conservant le paramètre de type.
Exemple 1 : Spécifier le type dans la sous-classe
// Classe générique de base
public class Boite<T> {
protected T contenu;
public Boite(T contenu) {
this.contenu = contenu;
}
public T getContenu() {
return contenu;
}
}
// Sous-classe qui spécifie le type (Boite de String)
public class BoiteString extends Boite<String> {
public BoiteString(String contenu) {
super(contenu);
}
// Méthodes spécifiques à String
public int longueur() {
return contenu.length(); // contenu est de type String
}
}
// Utilisation
BoiteString boite = new BoiteString("Hello");
String texte = boite.getContenu(); // Type garanti : String
int len = boite.longueur(); // Méthode spécifique
Exemple 2 : Conserver le paramètre de type
// Sous-classe générique qui conserve le paramètre de type
public class BoiteAvecLabel<T> extends Boite<T> {
private String label;
public BoiteAvecLabel(T contenu, String label) {
super(contenu);
this.label = label;
}
public String getLabel() {
return label;
}
}
// Utilisation
BoiteAvecLabel<String> boite1 = new BoiteAvecLabel<>("Hello", "Texte");
BoiteAvecLabel<Integer> boite2 = new BoiteAvecLabel<>(42, "Nombre");
⚠️ Compatibilité des types génériques
Les types génériques ne sont pas covariants. Cela signifie qu'une List<String> n'est pas un sous-type de List<Object>, même si String est un sous-type de Object.
Exemple : Problème de compatibilité
List<String> listeString = new ArrayList<>();
listeString.add("Hello");
// ❌ ERREUR : List<String> n'est pas un sous-type de List<Object>
// List<Object> listeObject = listeString; // Ne compile pas !
// Pourquoi ? Pour éviter ce problème :
// Si c'était possible, on pourrait faire :
// listeObject.add(123); // Ajouter un Integer dans une List<String> !
// String s = listeString.get(1); // ❌ ClassCastException !
Si List<String> était un sous-type de List<Object>, on pourrait ajouter n'importe quel Object dans une List<String>, ce qui violerait la sécurité de type. C'est pourquoi Java interdit cette conversion.
✅ Wildcards (Jokers) : ? extends et ? super
Les wildcards permettent de créer des relations de sous-typage avec les génériques. Il existe trois types de wildcards :
1. ? extends (Upper Bounded Wildcard)
Permet d'accepter un type ou n'importe quel sous-type.
// Méthode qui accepte une List de Number ou de ses sous-types
public static double somme(List<? extends Number> nombres) {
double total = 0.0;
for (Number n : nombres) {
total += n.doubleValue();
}
return total;
}
// Utilisation
List<Integer> entiers = Arrays.asList(1, 2, 3);
List<Double> decimaux = Arrays.asList(1.5, 2.5, 3.5);
double somme1 = somme(entiers); // ✅ OK : Integer extends Number
double somme2 = somme(decimaux); // ✅ OK : Double extends Number
// ⚠️ Important : Avec ? extends, vous ne pouvez que LIRE, pas ÉCRIRE
// nombres.add(10); // ❌ ERREUR : Type inconnu
2. ? super (Lower Bounded Wildcard)
Permet d'accepter un type ou n'importe quel super-type.
// Méthode qui accepte une List de Integer ou de ses super-types
public static void ajouterNombres(List<? super Integer> liste) {
liste.add(10); // ✅ OK : On peut ajouter un Integer
liste.add(20);
}
// Utilisation
List<Number> listeNumber = new ArrayList<>();
List<Object> listeObject = new ArrayList<>();
ajouterNombres(listeNumber); // ✅ OK : Number est super-type de Integer
ajouterNombres(listeObject); // ✅ OK : Object est super-type de Integer
// ⚠️ Important : Avec ? super, vous pouvez ÉCRIRE, mais la lecture retourne Object
// Number n = listeNumber.get(0); // ❌ ERREUR : Type inconnu
// Object o = listeNumber.get(0); // ✅ OK : Retourne Object
3. ? (Unbounded Wildcard)
Accepte n'importe quel type, mais avec des restrictions.
// Méthode qui accepte une List de n'importe quel type
public static void afficherTaille(List<?> liste) {
System.out.println("Taille : " + liste.size());
// ⚠️ On ne peut que lire comme Object
// Object o = liste.get(0); // ✅ OK
}
// Utilisation
List<String> chaines = Arrays.asList("a", "b");
List<Integer> nombres = Arrays.asList(1, 2);
afficherTaille(chaines); // ✅ OK
afficherTaille(nombres); // ✅ OK
📊 Résumé des wildcards
| Wildcard | Signification | Lecture | Écriture |
|---|---|---|---|
? extends T |
T ou sous-type de T | ✅ Comme T | ❌ Interdit |
? super T |
T ou super-type de T | ⚠️ Comme Object | ✅ Comme T |
? |
N'importe quel type | ⚠️ Comme Object | ❌ Interdit |
💡 Points clés à retenir
- Héritage : Une classe peut étendre une classe générique
- Type spécifique :
class BoiteString extends Boite<String> - Type générique :
class BoiteAvecLabel<T> extends Boite<T> - Non-covariance :
List<String>n'est pas un sous-type deList<Object> - ? extends : Pour lire (PECS : Producer Extends)
- ? super : Pour écrire (PECS : Consumer Super)
- ? : Type inconnu (lecture comme Object uniquement)
- Producer Extends : Si vous produisez/lisez des éléments, utilisez
? extends - Consumer Super : Si vous consommez/écrivez des éléments, utilisez
? super
8.3.5 – Méthodes génériques
Vous pouvez créer des méthodes génériques indépendamment de la classe. Une méthode générique peut être dans une classe générique ou non générique, et elle peut avoir ses propres paramètres de type.
📝 Structure de base
Pour créer une méthode générique, vous déclarez le paramètre de type avant le type de retour :
// Syntaxe générale
public <T> TypeRetour nomMethode(T parametre) {
// code
}
💻 Exemple 1 : Méthode générique simple
Exemple : Méthode utilitaire générique
public class Utilitaires {
// Méthode générique qui échange deux éléments
public static <T> void echanger(T[] tableau, int index1, int index2) {
T temp = tableau[index1];
tableau[index1] = tableau[index2];
tableau[index2] = temp;
}
// Méthode générique qui retourne le maximum
public static <T extends Comparable<T>> T maximum(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Méthode générique qui crée une liste à partir d'un tableau
public static <T> List<T> tableauVersListe(T[] tableau) {
List<T> liste = new ArrayList<>();
for (T element : tableau) {
liste.add(element);
}
return liste;
}
}
// Utilisation
public class TestUtilitaires {
public static void main(String[] args) {
// Échanger des String
String[] chaines = {"A", "B", "C"};
Utilitaires.echanger(chaines, 0, 2);
// Résultat : {"C", "B", "A"}
// Échanger des Integer
Integer[] nombres = {1, 2, 3};
Utilitaires.echanger(nombres, 0, 2);
// Résultat : {3, 2, 1}
// Maximum de deux String
String max = Utilitaires.maximum("Alice", "Bob");
// Résultat : "Bob"
// Maximum de deux Integer
Integer maxInt = Utilitaires.maximum(10, 20);
// Résultat : 20
}
}
💻 Exemple 2 : Méthode générique avec plusieurs paramètres de type
public class Utilitaires {
// Méthode qui crée une Paire à partir de deux valeurs
public static <T, U> Paire<T, U> creerPaire(T premier, U second) {
return new Paire<>(premier, second);
}
// Méthode qui copie une liste dans une autre
public static <T> void copier(List<? extends T> source, List<? super T> destination) {
for (T element : source) {
destination.add(element);
}
}
}
// Utilisation
Paire<String, Integer> paire = Utilitaires.creerPaire("Alice", 25);
List<Integer> source = Arrays.asList(1, 2, 3);
List<Number> destination = new ArrayList<>();
Utilitaires.copier(source, destination); // ✅ OK
💻 Exemple 3 : Méthode générique dans une classe non générique
// Classe non générique
public class TriUtilitaire {
// Méthode générique pour trier un tableau
public static <T extends Comparable<T>> void trier(T[] tableau) {
Arrays.sort(tableau);
}
// Méthode générique pour rechercher un élément
public static <T> int rechercher(T[] tableau, T element) {
for (int i = 0; i < tableau.length; i++) {
if (tableau[i].equals(element)) {
return i;
}
}
return -1;
}
}
// Utilisation
String[] chaines = {"Bob", "Alice", "Charlie"};
TriUtilitaire.trier(chaines); // Trie le tableau
int index = TriUtilitaire.rechercher(chaines, "Alice"); // Retourne 0
💻 Exemple 4 : Méthode générique dans une classe générique
// Classe générique
public class Boite<T> {
private T contenu;
public Boite(T contenu) {
this.contenu = contenu;
}
// Méthode générique avec son propre paramètre de type
public <U> Boite<U> convertir(Function<T, U> convertisseur) {
U nouveauContenu = convertisseur.apply(contenu);
return new Boite<>(nouveauContenu);
}
// Méthode générique statique
public static <E> Boite<E> creer(E element) {
return new Boite<>(element);
}
}
// Utilisation
Boite<String> boiteString = new Boite<>("123");
Boite<Integer> boiteInt = boiteString.convertir(Integer::parseInt);
// Méthode statique
Boite<String> boite = Boite.creer("Hello");
🔍 Inférence de type
Le compilateur Java peut souvent inférer le type du paramètre générique à partir du contexte, ce qui permet d'omettre le type explicite.
// Type explicite
String max1 = Utilitaires.<String>maximum("Alice", "Bob");
// Inférence de type (recommandé)
String max2 = Utilitaires.maximum("Alice", "Bob"); // Le compilateur infère String
// Dans certains cas, l'inférence n'est pas possible
List<String> liste = Utilitaires.<String>tableauVersListe(new String[]{"a", "b"});
// Ou avec inférence partielle
List<String> liste2 = Utilitaires.tableauVersListe(new String[]{"a", "b"});
📊 Comparaison : Méthode générique vs Classe générique
| Aspect | Méthode générique | Classe générique |
|---|---|---|
| Déclaration | public <T> void methode() |
public class Classe<T> |
| Portée | Seulement la méthode | Toute la classe |
| Utilisation | Méthodes utilitaires, conversions | Classes réutilisables avec types |
| Type spécifié | À chaque appel (ou inféré) | À la création de l'objet |
💡 Points clés à retenir
- Syntaxe :
public <T> TypeRetour methode() - Paramètres multiples :
public <T, U> TypeRetour methode() - Contraintes :
public <T extends Comparable<T>> T methode() - Statique : Les méthodes génériques peuvent être statiques
- Inférence : Le compilateur peut souvent inférer le type
- Indépendante : Une méthode générique peut être dans une classe non générique
- Paramètre propre : Une méthode générique peut avoir son propre paramètre de type différent de celui de la classe
- Utilisez des méthodes génériques pour des opérations utilitaires réutilisables
- Les méthodes génériques sont particulièrement utiles pour les conversions et transformations
- Laissez le compilateur inférer le type quand c'est possible (code plus lisible)
- Utilisez des contraintes (
extends) pour limiter les types acceptés - Les méthodes génériques statiques sont très utiles pour créer des classes utilitaires