CHAPITRE 8.3

Génériques

Créer des classes et méthodes paramétrées par type
Les génériques permettent de créer des classes, interfaces et méthodes qui fonctionnent avec différents types tout en conservant la sécurité de type. Ils éliminent le besoin de casting et améliorent la sécurité du code.

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);
Important : Évitez les raw types sauf pour la compatibilité avec d'ancien code. Utilisez toujours les génériques pour la sécurité de type.

💡 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
Conseils pratiques :
  • 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
    }
}
Analyse de la classe générique :
  • <T> : Déclare un paramètre de type nommé T
  • private T contenu; : Utilise T comme type de l'attribut
  • public 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
Règles importantes :
  • 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 !
Pourquoi les génériques ne sont pas covariants ?

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 de List<Object>
  • ? extends : Pour lire (PECS : Producer Extends)
  • ? super : Pour écrire (PECS : Consumer Super)
  • ? : Type inconnu (lecture comme Object uniquement)
Mémorisation PECS :
  • 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
Conseils pratiques :
  • 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