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 ?
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.
- 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
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
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.
<T> typeRetour nomMethode(T parametre) {
// corps
}
- 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);
}
}
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);
}
}
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)
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.
<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"));
}
}
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;
}
Lorsqu'on utilise plusieurs bornes avec &, la classe doit venir en premier, suivie des interfaces : <T extends MaClasse & MonInterface>.
4. Classes génériques
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>).
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);
}
}
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);
}
}
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));
}
}
(3, 4) distance: 5.0 (1, 1) distance: 1.4142135623730951 Comparaison p1 vs p2: 1
6. Wildcards (?)
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);
}
}
Liste de chaînes: A B C Liste d'entiers: 1 2 3 Somme: 6.0 Après ajout: [1, 2, 3, 4, 5]
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)
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; }
}
- 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());
}
}
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
Implémenter une pile générique (Stack) avec les opérations standards.
Travail demandé
- Créer une classe générique
Pile<T>avec :- Un tableau interne
Object[]ouArrayList<T> - Une capacité maximale (optionnelle)
- Méthodes :
push(T element),T pop(),T peek(),boolean isEmpty(),int size() - Gérer l'exception
EmptyStackExceptionpourpop()etpeek()
- Un tableau interne
- Ajouter une méthode générique statique
<T> void inverser(Pile<T> pile)qui inverse l'ordre des éléments - Tester avec différents types (Integer, String, Double)
import java.util.EmptyStackException;
import java.util.ArrayList;
import java.util.List;
/**
* Pile générique (Stack) implémentée avec ArrayList
*/
public class Pile<T> {
private List<T> elements;
private int capaciteMax;
// Constructeur sans capacité maximale
public Pile() {
this.elements = new ArrayList<>();
this.capaciteMax = -1; // -1 = illimitée
}
// Constructeur avec capacité maximale
public Pile(int capaciteMax) {
this.elements = new ArrayList<>();
this.capaciteMax = capaciteMax;
}
/**
* Ajoute un élément au sommet de la pile
*/
public void push(T element) {
if (capaciteMax > 0 && elements.size() >= capaciteMax) {
throw new IllegalStateException("Pile pleine ! Capacité maximale: " + capaciteMax);
}
elements.add(element);
}
/**
* Retire et retourne l'élément au sommet de la pile
*/
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
/**
* Retourne l'élément au sommet sans le retirer
*/
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
/**
* Vérifie si la pile est vide
*/
public boolean isEmpty() {
return elements.isEmpty();
}
/**
* Retourne le nombre d'éléments dans la pile
*/
public int size() {
return elements.size();
}
/**
* Vide la pile
*/
public void clear() {
elements.clear();
}
/**
* Méthode statique générique pour inverser une pile
*/
public static <E> void inverser(Pile<E> pile) {
Pile<E> temp = new Pile<>();
while (!pile.isEmpty()) {
temp.push(pile.pop());
}
while (!temp.isEmpty()) {
pile.push(temp.pop());
}
}
@Override
public String toString() {
return elements.toString();
}
}
/**
* Classe de test
*/
public class TestPile {
public static void main(String[] args) {
// Test avec des entiers
System.out.println("=== Pile d'entiers ===");
Pile<Integer> pileEntiers = new Pile<>(5);
pileEntiers.push(10);
pileEntiers.push(20);
pileEntiers.push(30);
pileEntiers.push(40);
pileEntiers.push(50);
System.out.println("Pile: " + pileEntiers);
System.out.println("Sommet: " + pileEntiers.peek());
System.out.println("Dépile: " + pileEntiers.pop());
System.out.println("Après dépilement: " + pileEntiers);
System.out.println("Taille: " + pileEntiers.size());
// Test avec des chaînes
System.out.println("\n=== Pile de chaînes ===");
Pile<String> pileChaines = new Pile<>();
pileChaines.push("Premier");
pileChaines.push("Deuxième");
pileChaines.push("Troisième");
System.out.println("Pile originale: " + pileChaines);
// Test de la méthode inverser
Pile.inverser(pileChaines);
System.out.println("Pile inversée: " + pileChaines);
// Test avec des doubles
System.out.println("\n=== Pile de doubles ===");
Pile<Double> pileDoubles = new Pile<>();
pileDoubles.push(3.14);
pileDoubles.push(2.718);
pileDoubles.push(1.414);
System.out.println("Pile: " + pileDoubles);
System.out.println("Est vide ? " + pileDoubles.isEmpty());
// Gestion des exceptions
System.out.println("\n=== Gestion des exceptions ===");
Pile<String> pileVide = new Pile<>();
try {
pileVide.pop();
} catch (EmptyStackException e) {
System.out.println("Exception attrapée: pile vide !");
}
}
}
=== Pile d'entiers === Pile: [10, 20, 30, 40, 50] Sommet: 50 Dépile: 50 Après dépilement: [10, 20, 30, 40] Taille: 4 === Pile de chaînes === Pile originale: [Premier, Deuxième, Troisième] Pile inversée: [Troisième, Deuxième, Premier] === Pile de doubles === Pile: [3.14, 2.718, 1.414] Est vide ? false === Gestion des exceptions === Exception attrapée: pile vide !
- Classe générique
Pile<T>: paramétrée par le type T des éléments. - Méthode statique générique
inverser(): utilise une pile temporaire pour inverser l'ordre. - Gestion des exceptions :
EmptyStackExceptionpour les piles vides. - Polymorphisme générique : la même classe fonctionne avec Integer, String, Double, etc.
L'essentiel en bref
- 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.
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.
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.