- Introduction aux Génériques Java ===================================
La généricité en Java, ou "types paramétrés", permet de définir des classes, des interfaces et des méthodes avec des paramètres de type. L'idée est de rendre les types d'une définition non pas concrets et fixes, mais variables, à l'image des paramètres d'une méthode. Lors de l'utilisation, ces paramètres de type sont remplacés par des types concrets.
Lo'bjectif principal des génériques est de renforcer la sécurité des types au moment de la compilation. En spécifiant les types de données qu'une structure peut manipuler, on réduit considérablement le risque d'erreurs d'exécution liées à des casts de type incorrects, sans avoir besoin de créer de nouvelles définitions de type pour chaque variant. Les données manipulées par une structure générique sont ainsi restreintes à un ensemble de types définis par les paramètres.
Il est important de noter que les génériques en Java opèrent principalement lors de la phase de compilation. Après une vérification rigoureuse des types, les informations génériques sont effacées (on parle de type erasure). Cela signifie que le code généré par le compilateur ne contient plus d'informations sur les types paramétrés ; les objets sont traités comme des types bruts (souvent Object), et des conversions de type implicites sont insérées là où nécessaire. Par conséquent, les informations génériques ne sont pas disponibles à l'exécution.
Enfin, un type générique ne peut pas être instancié directement. Par exemple, new T() n'est pas autorisé.
- Utilisation des Génériques =============================
Les génériques peuvent être appliqués de trois manières principales : aux classes, aux interfaces et aux méthodes.
2.1. Classes Génériques
Une classe générique est une classe qui accepte un ou plusieurs paramètres de type. Ces paramètres sont souvent représentés par des lettres majuscules comme T (Type), E (Element), K (Key), ou V (Value).
/**
* Définit une classe générique Boite<T> où T est un paramètre de type.
* Le type concret de T doit être spécifié lors de l'instanciation de la classe.
*/
public class Boite<T> {
// La variable membre 'contenu' est du type T, spécifié à l'extérieur.
private T contenu;
/**
* Constructeur générique. Le type du paramètre 'element' est également T.
*/
public Boite(T element) {
this.contenu = element;
}
/**
* Méthode générique retournant le contenu de type T.
*/
public T getContenu() {
return contenu;
}
public void setContenu(T contenu) {
this.contenu = contenu;
}
}
2.2. Interfaces Génériques
La définition et l'utilisation des interfaces génériques sont très similaires à celles des classes génériques.
// Définit une interface générique pour une fabrique
public interface Fabrique<T> {
T produire(); // Méthode qui produit un objet de type T
}
Implémentation sans Spécifier le Type Générique
Lorsque la classe d'implémentation ne spécifie pas de type concret pour le paramètre générique de l'interface, elle doit elle-même être déclarée générique.
/**
* Implémentation générique de Fabrique<T>.
* Le paramètre de type T est propagé à la classe d'implémentation.
*/
class GenerateurGenerique<T> implements Fabrique<T> {
@Override
public T produire() {
// Retourne null ou une implémentation par défaut
return null;
}
}
Implémentation avec un Type Spécifique
Si la classe d'implémentation spécifie un type concret pour le paramètre générique de l'interface, toutes les occurrences de ce paramètre de type dans l'interface doivent être remplacées par ce type concret dans la classe d'implémentation.
import java.util.Random;
/**
* Implémentation concrète de Fabrique<String>.
* Le type String remplace T partout dans l'interface.
*/
public class GenerateurDeFruits implements Fabrique<String> {
private String[] varietes = {"Pomme", "Banane", "Poire"};
private Random rand = new Random();
@Override
public String produire() {
return varietes[rand.nextInt(varietes.length)];
}
}
2.3. Méthodes Génériques
Contrairement aux classes génériques où le type est spécifié à l'instanciation de la classe, les méthodes génériques spécifient leur type au moment de leur appel. Une méthode générique peut appartenir à une classe non générique, ou à une classe générique.
public class UtilitaireGenerique {
/**
* Méthode générique simple qui instancie un objet du type T.
* Le <T> avant le type de retour 'T' déclare cette méthode comme générique.
* C'est crucial pour que 'T' soit reconnu comme un paramètre de type ici.
*/
public <T> T instancierViaClasse(Class<T> typeClasse) throws InstantiationException, IllegalAccessException {
// Crée une nouvelle instance du type T
return typeClasse.newInstance();
}
/**
* Méthode générique avec un nombre variable d'arguments (varargs).
* @param premier Le premier élément de type T.
* @param autres Un tableau d'éléments de type T.
*/
public <T> void afficherMessage(T premier, T... autres) {
System.out.println("Premier élément : " + premier);
for (T element : autres) {
System.out.println("Autre élément : " + element);
}
}
}
Méthodes Statiques et Génériques
Une méthode statique, même au sein d'une classe générique, ne peut pas utiliser les paramètres de type de la classe. Si une méthode statique doit être générique, elle doit déclarer ses propres paramètres de type.
public class Conteneur<V> {
// Une méthode d'instance peut utiliser le paramètre de type de la classe (V).
public V obtenirValeur(V valeur) {
System.out.println("Méthode d'instance avec type de classe : " + valeur);
return valeur;
}
// ERREUR DE COMPILATION : Une méthode statique ne peut pas directement utiliser le paramètre de type de la classe (V).
// public static V afficherValeurStatiqueErreur(V valeur) { ... }
// Correct : Pour qu'une méthode statique soit générique, elle doit déclarer son propre paramètre de type.
public static <U> U afficherValeurStatique(U valeur) {
System.out.println("Méthode statique générique : " + valeur);
return valeur;
}
}
- Les Wildcards Génériques (
?) =================================
Un problème courant avec les génériques est que, bien que Integer soit un sous-type de Number, Boite<Integer> n'est pas considéré comme un sous-type de Boite<Number>. Cela signifie que vous ne pouvez pas passer une Boite<Integer> à une méthode qui attend une Boite<Number>.
public class DemoWildcard {
// Cette méthode attend spécifiquement une Boite de type Number.
public void afficherContenuNombre(Boite<Number> boite) {
System.out.println("Contenu de la boite (Number) : " + boite.getContenu());
}
public static void main(String[] args) {
Boite<Integer> boiteEntier = new Boite<>(123);
Boite<Number> boiteNombre = new Boite<>(45.67);
DemoWildcard demo = new DemoWildcard();
demo.afficherContenuNombre(boiteNombre); // OK
// ERREUR DE COMPILATION : Boite<Integer> n'est pas compatible avec Boite<Number>
// demo.afficherContenuNombre(boiteEntier);
}
}
Pour résoudre ce type de problème et permettre une plus grande flexibilité, Java introduit le wildcard ?. Le wildcard représente un type inconnu, et il est souvent utilisé lorsque le type exact n'importe pas, ou lorsque l'on veut exprimer une relation de sous-typage plus complexe.
public class DemoWildcard {
// Cette méthode accepte une Boite de n'importe quel type (type inconnu).
public void afficherContenuGenerique(Boite<?> boite) {
System.out.println("Contenu de la boite (générique) : " + boite.getContenu());
// Vous ne pouvez pas ajouter de nouveaux éléments à une Boite<?> de manière sûre
// boite.setContenu(new Object()); // Erreur de compilation
}
public static void main(String[] args) {
Boite<Integer> boiteEntier = new Boite<>(123);
Boite<String> boiteChaine = new Boite<>("Bonjour");
DemoWildcard demo = new DemoWildcard();
demo.afficherContenuGenerique(boiteEntier); // OK
demo.afficherContenuGenerique(boiteChaine); // OK
}
}
Le wildcard ? est utile lorsque vous devez travailler avec des collections ou des types génériques dont vous ne connaissez pas le type exact, mais que vous ne prévoyez que de lire les éléments (ou d'appeler des méthodes dont le type de retour est indépendant du paramètre de type).
- Bornes de Type Génériques ============================
Les wildcards peuvent être bornés pour restreindre l'ensemble des types qu'ils peuvent représenter. Il existe deux types de bornes : la borne supérieure (extends) et la borne inférieure (super).
4.1. Borne Supérieure (? extends T)
? extends T signifie "n'importe quel type qui est T ou un sous-type de T". Cela permet à une méthode d'accepter des génériques avec des types qui sont des spécialisations d'un type donné. Les collections avec une borne supérieure sont principalement "productrices" d'éléments (vous pouvez les lire, mais pas y ajouter d'éléments de manière sûre, sauf null).
public class GestionnaireNumerique {
/**
* Accepte une Boite dont le contenu est de type Number ou un sous-type de Number (ex: Integer, Float, Double).
* On peut lire le contenu comme un Number.
*/
public static void traiterContenuNumerique(Boite<? extends Number> boite) {
Number n = boite.getContenu(); // Lire est sûr (le contenu est au moins un Number)
System.out.println("Valeur numérique traitée : " + n);
// boite.setContenu(new Integer(10)); // ERREUR DE COMPILATION : écriture non sécurisée
// On ne sait pas si la boîte contient des Integers, des Doubles, etc.
}
public static void main(String[] args) {
Boite<Integer> boiteInt = new Boite<>(100);
Boite<Float> boiteFloat = new Boite<>(5.5f);
Boite<Double> boiteDouble = new Boite<>(9.99);
// Boite<String> boiteString = new Boite<>("texte"); // Cette ligne provoquerait une erreur si utilisée avec traiterContenuNumerique
traiterContenuNumerique(boiteInt); // OK
traiterContenuNumerique(boiteFloat); // OK
traiterContenuNumerique(boiteDouble); // OK
}
}
4.2. Borne Inférieure (? super T)
? super T signifie "n'importe quel type qui est T ou un super-type de T". Cela permet d'accepter des génériques avec des types qui sont des généralisations d'un type donné. Les collections avec une borne inférieure sont principalement des "consommatrices" d'éléments (vous pouvez y ajouter des éléments de type T ou de ses sous-types, mais la lecture est limitée au type Object).
public class GestionnaireNumerique {
/**
* Accepte une Boite dont le contenu est de type Integer ou un super-type de Integer (ex: Number, Object).
* On peut ajouter des Integer (ou des sous-types d'Integer si cela existait) à cette Boite.
*/
public static void ajouterEntier(Boite<? super Integer> boite, Integer valeur) {
boite.setContenu(valeur); // Écrire est sûr (tout Integer peut être stocké dans Integer, Number, Object)
System.out.println("Ajouté " + valeur + " à la boite.");
// Object obj = boite.getContenu(); // La lecture est limitée à Object pour garantir la sécurité
// System.out.println("Contenu (lu comme Object) : " + obj);
}
public static void main(String[] args) {
Boite<Integer> boiteSpecifique = new Boite<>(0);
Boite<Number> boiteGenerale = new Boite<>(0.0);
Boite<Object> boiteTresGenerale = new Boite<>(null);
ajouterEntier(boiteSpecifique, 50); // OK, boîte d'Integers
ajouterEntier(boiteGenerale, 120); // OK, boîte de Numbers (Integer est un Number)
ajouterEntier(boiteTresGenerale, 250); // OK, boîte d'Objects (Integer est un Object)
System.out.println("Contenu de boiteSpecifique après ajout : " + boiteSpecifique.getContenu());
System.out.println("Contenu de boiteGenerale après ajout : " + boiteGenerale.getContenu());
System.out.println("Contenu de boiteTresGenerale après ajout : " + boiteTresGenerale.getContenu());
}
}