Aller au contenu principal

Les génériques (Generics)

Notions théoriques

Pourquoi les génériques ?

Sans génériques, une collection Java accepte n'importe quel type d'objet :

List liste = new ArrayList();
liste.add(new Guerrier("Thor", 120, 18, "épée"));
liste.add("texte par erreur"); // Java ne dit rien !

Guerrier g = (Guerrier) liste.get(1); // ClassCastException à l'exécution !

Les génériques permettent de spécifier le type d'une collection à la compilation, éliminant les casts et les erreurs d'exécution :

List<Guerrier> guerriers = new ArrayList<>();
guerriers.add(new Guerrier("Thor", 120, 18, "épée"));
// guerriers.add("texte"); // ERREUR de compilation immédiate !

Guerrier g = guerriers.get(0); // Pas besoin de cast

Syntaxe des génériques : <T>

T est un paramètre de type : un nom qui représente un type concret fourni à l'utilisation. Par convention :

  • T = Type générique (le plus courant)
  • E = Element (dans les collections)
  • K / V = Key / Value (dans les maps)

Créer une classe générique

public class Inventaire<T> {
private List<T> items = new ArrayList<>();

public void ajouter(T item) {
items.add(item);
}

public T recuperer(int index) {
return items.get(index);
}

public int taille() {
return items.size();
}
}

Utilisation :

Inventaire<Personnage> equipe = new Inventaire<>();
equipe.ajouter(new Guerrier("Thor", 120, 18, "épée"));
equipe.ajouter(new Mage("Elara", 80, 12, 60));

Personnage p = equipe.recuperer(0); // Pas de cast

Méthode générique

Une méthode peut avoir son propre paramètre de type, indépendamment de la classe :

public <T> T premier(List<T> liste) {
if (liste.isEmpty()) return null;
return liste.get(0);
}

// Utilisation :
Guerrier premier = premier(guerriers); // T est inféré automatiquement

Bornes : <T extends Personnage>

On peut restreindre le type acceptable avec extends : T doit être Personnage ou une sous-classe de Personnage.

public class Inventaire<T extends Personnage> {
private List<T> items = new ArrayList<>();

// On peut appeler les méthodes de Personnage sur T
public T trouverPlusFort() {
T plusFort = null;
for (T item : items) {
if (plusFort == null || item.getForce() > plusFort.getForce()) {
plusFort = item;
}
}
return plusFort;
}
}

Sans borne <T extends Personnage>, on ne pourrait pas appeler item.getForce() car Java ne saurait pas que T possède cette méthode.

Wildcard <?> : en lecture seule

Le wildcard <?> représente un type inconnu. Utile pour lire des collections de n'importe quel type.

// Afficher n'importe quelle liste, quel que soit le type
public void afficherTout(List<?> liste) {
for (Object item : liste) {
System.out.println(item);
}
}

// On peut passer une List<Guerrier> ou une List<Mage>
afficherTout(guerriers);
afficherTout(mages);
attention

On ne peut pas ajouter d'éléments à une List<?> (sauf null). Le wildcard est réservé à la lecture.

Collections.sort() avec Comparable<T>

Pour trier une liste d'objets, la classe doit implémenter Comparable<T> et définir compareTo().

public class Personnage implements Comparable<Personnage> {

@Override
public int compareTo(Personnage autre) {
// Trier par force décroissante
return autre.getForce() - this.getForce();
}
}

// Utilisation :
List<Personnage> equipe = new ArrayList<>();
// ... ajouter des personnages ...
Collections.sort(equipe); // trie par force décroissante

Exemple pratique

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

// Fichier : Inventaire.java
public class Inventaire<T extends Personnage> {
private String nom;
private List<T> items = new ArrayList<>();

public Inventaire(String nom) {
this.nom = nom;
}

public void ajouter(T item) {
items.add(item);
System.out.println(item.getNom() + " ajouté à l'inventaire '" + nom + "'.");
}

public T recuperer(int index) {
return items.get(index);
}

public int taille() {
return items.size();
}

public T trouverPlusFort() {
if (items.isEmpty()) return null;
T plusFort = items.get(0);
for (T item : items) {
if (item.getForce() > plusFort.getForce()) {
plusFort = item;
}
}
return plusFort;
}

public void afficherTout() {
System.out.println("=== Inventaire : " + nom + " (" + items.size() + " personnages) ===");
for (T item : items) {
System.out.println(" " + item);
}
}
}

// Fichier : Main.java
public class Main {
public static void main(String[] args) {
// Inventaire de Personnages (n'importe quel type de personnage)
Inventaire<Personnage> equipe = new Inventaire<>("Équipe héros");
equipe.ajouter(new Guerrier("Thor", 120, 18, "épée runique"));
equipe.ajouter(new Mage("Elara", 80, 12, 60));
equipe.ajouter(new Guerrier("Brunhild", 130, 22, "hache de guerre"));

equipe.afficherTout();

Personnage plusFort = equipe.trouverPlusFort();
System.out.println("\nLe plus fort : " + plusFort.getNom() + " (force: " + plusFort.getForce() + ")");

// Inventaire spécialisé (uniquement des Guerriers)
Inventaire<Guerrier> brigade = new Inventaire<>("Brigade d'assaut");
brigade.ajouter(new Guerrier("Thor", 120, 18, "épée"));
brigade.ajouter(new Guerrier("Odin", 150, 25, "lance"));

Guerrier chef = brigade.trouverPlusFort();
System.out.println("\nChef de brigade : " + chef.getNom() + " (arme: " + chef.getTypeArme() + ")");
}
}
info

Inventaire<Personnage> accepte n'importe quelle sous-classe de Personnage. Inventaire<Guerrier> n'accepte que des Guerrier. Les deux utilisent la même classe générique Inventaire<T>.

Test de mémorisation/compréhension


Quel est le principal avantage des génériques en Java ?


Que signifie <T extends Personnage> dans la déclaration d'une classe générique ?


Peut-on ajouter des éléments à une List<?> ?


Qu'est-ce qu'un 'raw type' en Java ?


Quelle interface doit implémenter une classe pour être triable avec Collections.sort() ?


Comment appelle-t-on <T> dans 'public class Inventaire<T>' ?


TP pour réfléchir et résoudre des problèmes

Vous allez créer la classe Inventaire<T extends Personnage> avec une méthode trouverPlusFort() et l'utiliser dans Main.

Étape 1 — Déclarer la classe Inventaire générique

Créez Inventaire.java avec le paramètre de type borné <T extends Personnage>.


Bonne pratique - Borner les génériques dès que possible

Sans borne <T extends Personnage>, la méthode trouverPlusFort() ne pourrait pas appeler item.getForce() car Java ne saurait pas que T a cette méthode. La borne informe le compilateur des garanties sur T et évite des casts.

Étape 2 — Implémenter trouverPlusFort()

Ajoutez la méthode trouverPlusFort() qui parcourt la liste et retourne le personnage avec la plus grande force.


Bonne pratique - Retourner null pour une collection vide

Retourner null quand la collection est vide est une convention acceptable, mais en Java moderne on préférerait Optional<T>. Pour l'instant, vérifiez toujours que le résultat de trouverPlusFort() n'est pas null avant de l'utiliser.

Étape 3 — Utiliser Inventaire dans Main

Créez un Inventaire<Personnage>, ajoutez-y des personnages de types différents, et affichez le plus fort.


Bonne pratique - Spécifier le type le plus général utile

Inventaire<Personnage> est plus flexible qu'Inventaire<Guerrier> car il accepte tous les types de personnages. Choisissez le type le plus général qui répond à votre besoin. Si vous avez besoin des méthodes spécifiques à Guerrier (ex: getTypeArme()), alors utilisez Inventaire<Guerrier>.

📌 Une solution