Classes et méthodes génériques

13 Sep 2019 13 Sep 2019 5372 vues ESSADDOUKI Mostafa 18 min de lecture
Introduction
1 Nouveautés de Java 11 2 Différences entre JDK, JRE et JVM 3 Structure d'un programme Java - Hello World 4 Mots clés et conventions de dénomination en Java 5 Types de données intégrés en Java 6 Les variables en Java 7 Classes enveloppe - Number, Integer, Double ... 8 Lire les entrées clavier en Java
Structures de contrôle
9 Les opérateurs en Java 10 Les structures conditionnelles en Java 11 Les boucles en Java 12 Instructions de contrôle de boucle - break, continue
Chaines de caractères
13 Les chaines en Java - API String 14 Les chaines en Java - StringBuffer et StringBuilder 15 Les expressions régulières en Java
Programmation OO
16 Objets et classes en Java 17 Modificateurs d'accès Java - public, private, protected et package 18 Méthodes et surcharge des méthodes en Java 19 les constructeurs en Java 20 L'héritage en Java 21 Classes abstraites en Java 22 Interfaces et héritage multiple en Java 23 Les classes imbriquées en Java 24 Les singletons en Java 25 Classes et méthodes génériques 26 Interface fonctionnelle et expressions Lambda en Java
Tableaux et collections
27 Les tableaux en Java 28 Classe Arrays - java.util.Arrays 29 Les listes dynamiques - java.util.ArrayList 30 Les listes chaînées en Java - java.util.LinkedList 31 HashSet en Java - java.util.HashSet 32 HashMap en Java - java.util.HashMap
Gestion des fichiers
33 Comprendre les fichiers informatiques 34 Utilisation des classes Path et Files en Java 35 Lecture et écriture dans un fichier en Java 36 Fichiers à accès aléatoire en Java
Gestion d'exceptions
37 Gestion d'exceptions en Java 38 Créez vos propres classes d'exception en Java
Programmation concurrente
39 Introduction à la programmation concurrente en Java - Multi-threads 40 classe java.lang.Thread 41 Synchronisation des threads en Java
Cours Java pour les débutants — Étape 25 sur 41

Génériques en Java

  Prérequis

Maîtriser les classes, les méthodes et l'héritage. Comprendre les collections Java (ArrayList, HashMap). Connaître les interfaces Comparable et Comparator.

  Objectifs

Comprendre l'utilité des génériques pour la réutilisabilité et la sécurité de type. Maîtriser la création de classes, méthodes et interfaces génériques. Savoir utiliser les paramètres de type bornés (extends). Connaître le principe de type erasure (effacement des types).

1. Problématique — Pourquoi les génériques ?

Génériques

Les génériques (ou types paramétrés) permettent aux programmeurs de spécifier, avec une seule déclaration de méthode ou de classe, un ensemble de méthodes ou de types associés. Ils offrent une sécurité de type à la compilation et éliminent les casts explicites.

Imaginez que nous voulions écrire une méthode de tri qui fonctionne aussi bien sur un tableau d'Integer, de String ou de tout type prenant en charge l'ordre. Sans les génériques, nous devrions écrire une méthode pour chaque type ou utiliser Object avec des casts dangereux.

Avantages des génériques
  • Réutilisabilité : une classe/méthode peut être utilisée avec différents types
  • Sécurité de type : les erreurs de type sont détectées à la compilation, pas à l'exécution
  • Pas de cast explicite : le code est plus propre et plus lisible
  • Documentation : le code indique clairement quels types sont attendus
Analogie avec C++

Les génériques en Java sont similaires aux templates en C++, mais avec une différence fondamentale : en Java, les types génériques sont effacés à la compilation (type erasure) alors qu'en C++, chaque instanciation crée un nouveau type.

2. Méthodes génériques

Méthode générique

Une méthode générique est une méthode qui introduit ses propres paramètres de type. Elle peut être appelée avec des arguments de différents types et le compilateur adapte le comportement en conséquence.

   
Syntaxe — Méthode générique Java
<T> typeRetour nomMethode(T parametre) {
    // corps
}
Règles de déclaration
  • La section de paramètres de type (<T>) précède le type de retour
  • Les paramètres de type ne peuvent représenter que des types référence (pas int, double, etc.)
  • On peut utiliser plusieurs paramètres : <K, V>
  • Par convention, on utilise des lettres majuscules : T (Type), E (Element), K (Key), V (Value)

  Exemple 1 — Méthode générique d'affichage

public class Test {
    // Méthode générique qui affiche tous les éléments d'un tableau
    public <E> void afficher(E[] tableau) {
        for (E element : tableau) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Test test = new Test();

        Integer[] entiers = {1, 4, 5, 8, 12};
        Double[] decimaux = {2.4, 5.7, 7.0, 3.14};
        String[] chaines = {"Mostafa", "Ismail", "Kaoutar", "Amal"};

        test.afficher(entiers);
        test.afficher(decimaux);
        test.afficher(chaines);
    }
}
Sortie
1 4 5 8 12 
2.4 5.7 7.0 3.14 
Mostafa Ismail Kaoutar Amal

  Exemple 2 — Méthode avec plusieurs paramètres de type

public class Test {
    // Méthode avec deux types génériques différents
    public <K, V> void afficherPaire(K cle, V valeur) {
        System.out.println("Clé: " + cle + " → Valeur: " + valeur);
    }

    public static void main(String[] args) {
        Test test = new Test();

        test.afficherPaire(1, "Mostafa");
        test.afficherPaire("email", "mostafa@example.com");
        test.afficherPaire(3.14, Math.PI);
    }
}
Sortie
Clé: 1 → Valeur: Mostafa
Clé: email → Valeur: mostafa@example.com
Clé: 3.14 → Valeur: 3.141592653589793

3. Paramètres de type bornés (Bounded Type Parameters)

Paramètre de type borné

Un paramètre de type borné restreint les types qui peuvent être utilisés comme arguments. On utilise le mot-clé extends (pour les classes et les interfaces) suivi de la limite supérieure.

Il peut être nécessaire de restreindre les types passés à un paramètre. Par exemple, une méthode qui calcule le maximum ne devrait accepter que des types qui implémentent Comparable.

   
Syntaxe — Type borné Java
<T extends SuperType>   // T doit être un sous-type de SuperType

  Exemple 3 — Méthode générique avec borne Comparable

public class Test {
    // T doit implémenter l'interface Comparable<T>
    public <T extends Comparable<T>> T maximum(T x, T y, T z) {
        T max = x;

        if (y.compareTo(max) > 0) {
            max = y;
        }
        if (z.compareTo(max) > 0) {
            max = z;
        }
        return max;
    }

    public static void main(String[] args) {
        Test test = new Test();

        System.out.println("Max de (2, 7, 5) : " + test.maximum(2, 7, 5));
        System.out.println("Max de (6.35, 2.6, 1.25) : " + test.maximum(6.35, 2.6, 1.25));
        System.out.println("Max de (pomme, banane, cerise) : " 
            + test.maximum("pomme", "banane", "cerise"));
    }
}
Sortie
Max de (2, 7, 5) : 7
Max de (6.35, 2.6, 1.25) : 6.35
Max de (pomme, banane, cerise) : pomme
extends pour les classes et interfaces

On utilise toujours le mot-clé extends même pour les interfaces. <T extends Comparable<T>> signifie que T doit implémenter l'interface Comparable.

  Exemple 4 — Borne multiple

// T doit être à la fois un Number ET implémenter Comparable
public <T extends Number & Comparable<T>> T traiterNombre(T valeur) {
    System.out.println("Valeur: " + valeur + ", Type: " + valeur.getClass().getSimpleName());
    return valeur;
}
Borne avec &

Lorsqu'on utilise plusieurs bornes avec &, la classe doit venir en premier, suivie des interfaces : <T extends MaClasse & MonInterface>.

4. Classes génériques

Classe générique

Une classe générique est une classe paramétrée par un ou plusieurs types. Le nom de la classe est suivi d'une section de paramètres de type entre crochets (<T>).

   
Syntaxe — Classe générique Java
public class NomClasse<T> {
    private T attribut;

    public NomClasse(T attribut) {
        this.attribut = attribut;
    }

    public T getAttribut() {
        return attribut;
    }

    public void setAttribut(T attribut) {
        this.attribut = attribut;
    }
}

  Exemple 5 — Classe générique simple

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 boolean estVide() {
        return contenu == null;
    }

    @Override
    public String toString() {
        return "Boite[" + contenu + "]";
    }
}

public class TestBoite {
    public static void main(String[] args) {
        Boite<Integer> boiteEntier = new Boite<>(42);
        Boite<String> boiteChaine = new Boite<>("Hello");
        Boite<Double> boiteDouble = new Boite<>(3.14159);

        System.out.println(boiteEntier);
        System.out.println(boiteChaine);
        System.out.println(boiteDouble);
    }
}
Sortie
Boite[42]
Boite[Hello]
Boite[3.14159]

  Exemple 6 — Classe générique avec deux paramètres (Paire)

public class Paire<K, V> {
    private K cle;
    private V valeur;

    public Paire(K cle, V valeur) {
        this.cle = cle;
        this.valeur = valeur;
    }

    public K getCle() { return cle; }
    public V getValeur() { return valeur; }

    public void setCle(K cle) { this.cle = cle; }
    public void setValeur(V valeur) { this.valeur = valeur; }

    @Override
    public String toString() {
        return "Paire{" + cle + "=" + valeur + "}";
    }
}

public class TestPaire {
    public static void main(String[] args) {
        Paire<Integer, String> p1 = new Paire<>(1, "Mostafa");
        Paire<String, Double> p2 = new Paire<>("PI", 3.14159);
        Paire<String, String> p3 = new Paire<>("pays", "Maroc");

        System.out.println(p1);
        System.out.println(p2);
        System.out.println(p3);
    }
}
Sortie
Paire{1=Mostafa}
Paire{PI=3.14159}
Paire{pays=Maroc}

5. Interfaces génériques

  Exemple 7 — Interface générique Comparable (intégrée à Java)

// L'interface Comparable est générique
public interface Comparable<T> {
    int compareTo(T autre);
}

// Implémentation avec une classe générique
public class Point<T extends Number> implements Comparable<Point<T>> {
    private T x;
    private T y;

    public Point(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public double distanceOrigine() {
        return Math.sqrt(x.doubleValue() * x.doubleValue() + 
                        y.doubleValue() * y.doubleValue());
    }

    @Override
    public int compareTo(Point<T> autre) {
        return Double.compare(this.distanceOrigine(), autre.distanceOrigine());
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

public class TestPoint {
    public static void main(String[] args) {
        Point<Integer> p1 = new Point<>(3, 4);
        Point<Integer> p2 = new Point<>(1, 1);
        Point<Double> p3 = new Point<>(2.5, 2.5);

        System.out.println(p1 + " distance: " + p1.distanceOrigine());
        System.out.println(p2 + " distance: " + p2.distanceOrigine());
        System.out.println("Comparaison p1 vs p2: " + p1.compareTo(p2));
    }
}
Sortie
(3, 4) distance: 5.0
(1, 1) distance: 1.4142135623730951
Comparaison p1 vs p2: 1

6. Wildcards (?)

Wildcard (?)

Le wildcard (?) représente un type inconnu. Il est utilisé dans les paramètres de type pour indiquer qu'un type peut être accepté sans le spécifier exactement.

  Exemple 8 — Wildcard unbounded (non borné)

import java.util.ArrayList;
import java.util.List;

public class WildcardDemo {
    // Accepte n'importe quelle liste, quel que soit son type
    public static void afficherListe(List<?> liste) {
        for (Object element : liste) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    // Wildcard borné supérieur (extends)
    public static double somme(List<? extends Number> nombres) {
        double somme = 0;
        for (Number n : nombres) {
            somme += n.doubleValue();
        }
        return somme;
    }

    // Wildcard borné inférieur (super)
    public static void ajouterEntiers(List<? super Integer> liste) {
        for (int i = 1; i <= 5; i++) {
            liste.add(i);
        }
    }

    public static void main(String[] args) {
        List<String> chaines = new ArrayList<>();
        chaines.add("A");
        chaines.add("B");
        chaines.add("C");

        List<Integer> entiers = new ArrayList<>();
        entiers.add(1);
        entiers.add(2);
        entiers.add(3);

        System.out.print("Liste de chaînes: ");
        afficherListe(chaines);

        System.out.print("Liste d'entiers: ");
        afficherListe(entiers);

        System.out.println("Somme: " + somme(entiers));

        List<Number> nombres = new ArrayList<>();
        ajouterEntiers(nombres);
        System.out.println("Après ajout: " + nombres);
    }
}
Sortie
Liste de chaînes: A B C 
Liste d'entiers: 1 2 3 
Somme: 6.0
Après ajout: [1, 2, 3, 4, 5]
Types de wildcards
  • List<?> — type inconnu (unbounded wildcard)
  • List<? extends T> — type inconnu qui est un sous-type de T (upper bounded)
  • List<? super T> — type inconnu qui est un super-type de T (lower bounded)

7. Type Erasure (Effacement des types)

Type Erasure

Java implémente les génériques via l'effacement des types. À la compilation, les paramètres de type sont supprimés et remplacés par leurs bornes (Object si aucune borne n'est spécifiée). Cela assure la compatibilité avec le code antérieur à Java 5.

  Exemple 9 — Ce qui se passe à la compilation

// Code source
public class MaClasse<T> {
    private T valeur;
    public T getValeur() { return valeur; }
}

// Après effacement (équivalent bytecode)
public class MaClasse {
    private Object valeur;
    public Object getValeur() { return valeur; }
}
Limitations du type erasure
  • On ne peut pas instancier un type générique : new T() interdit
  • On ne peut pas créer un tableau de type générique : new T[10] interdit
  • On ne peut pas utiliser les types primitifs (utiliser les wrappers)
  • On ne peut pas avoir de méthodes statiques génériques dans une classe générique (la statique est partagée entre toutes les instances)

8. Exemple complet — Classe générique Repository

  Exemple 10 — Repository générique pour la persistance

import java.util.*;

/**
 * Repository générique pour gérer des entités de tout type
 */
public class Repository<T, ID> {
    private Map<ID, T> storage;
    private int nextId;

    public Repository() {
        this.storage = new HashMap<>();
        this.nextId = 1;
    }

    public T save(T entity) {
        // Dans un vrai repository, on générerait un ID automatique
        // Ici, on suppose que l'entité a déjà un ID
        storage.put((ID) entity.getClass().getMethod("getId").invoke(entity), entity);
        return entity;
    }

    public T findById(ID id) {
        return storage.get(id);
    }

    public List<T> findAll() {
        return new ArrayList<>(storage.values());
    }

    public void deleteById(ID id) {
        storage.remove(id);
    }

    public boolean existsById(ID id) {
        return storage.containsKey(id);
    }

    public long count() {
        return storage.size();
    }

    public void clear() {
        storage.clear();
    }
}

// Classe entité générique
class Entity<ID> {
    private ID id;
    private String nom;

    public Entity(ID id, String nom) {
        this.id = id;
        this.nom = nom;
    }

    public ID getId() { return id; }
    public String getNom() { return nom; }
    public void setNom(String nom) { this.nom = nom; }

    @Override
    public String toString() {
        return "Entity{id=" + id + ", nom='" + nom + "'}";
    }
}

public class TestRepository {
    public static void main(String[] args) {
        Repository<Entity<Integer>, Integer> repo = new Repository<>();

        Entity<Integer> e1 = new Entity<>(1, "Mostafa");
        Entity<Integer> e2 = new Entity<>(2, "Amal");
        Entity<Integer> e3 = new Entity<>(3, "Youssef");

        repo.save(e1);
        repo.save(e2);
        repo.save(e3);

        System.out.println("Count: " + repo.count());
        System.out.println("Find by id 2: " + repo.findById(2));
        System.out.println("All entities: " + repo.findAll());
    }
}
Sortie
Count: 3
Find by id 2: Entity{id=2, nom='Amal'}
All entities: [Entity{id=1, nom='Mostafa'}, Entity{id=2, nom='Amal'}, Entity{id=3, nom='Youssef'}]

9. Exercice

Implémentation d'une classe générique Stack

Niveau : Intermédiaire

Implémenter une pile générique (Stack) avec les opérations standards.

Travail demandé

  1. Créer une classe générique Pile<T> avec :
    • Un tableau interne Object[] ou ArrayList<T>
    • Une capacité maximale (optionnelle)
    • Méthodes : push(T element), T pop(), T peek(), boolean isEmpty(), int size()
    • Gérer l'exception EmptyStackException pour pop() et peek()
  2. Ajouter une méthode générique statique <T> void inverser(Pile<T> pile) qui inverse l'ordre des éléments
  3. Tester avec différents types (Integer, String, Double)

  L'essentiel en bref

Synthèse — Génériques en Java
  • Les génériques permettent d'écrire du code réutilisable et type-safe.
  • Une classe générique : public class MaClasse<T>
  • Une méthode générique : public <T> void maMethode(T param)
  • Un paramètre borné : <T extends Number> restreint les types acceptés.
  • Le wildcard ? représente un type inconnu : List<?>, List<? extends Number>, List<? super Integer>.
  • Type erasure : les types génériques sont effacés à la compilation pour compatibilité.
  • Avantages : réutilisabilité, sécurité de type, pas de cast explicite.
  • Limitations : pas de types primitifs, pas d'instanciation de T, pas de tableaux génériques.
Un peu d'histoire

Les génériques ont été introduits en Java avec la version Java 5 (J2SE 5.0) en 2004, dans le cadre de la JSR 14. Leur conception a été influencée par les templates du C++ et les génériques d'Eiffel. L'implémentation par type erasure a été choisie pour assurer la compatibilité binaire avec les versions antérieures de Java. Sans les génériques, les collections Java stockaient des Object, ce qui nécessitait des casts dangereux. Les génériques ont été une avancée majeure pour la sécurité de type du langage.

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.