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);
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() + ")");
}
}
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
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>.
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.
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.
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>.