Notes S2
Généricité en Java
Idée clé: éviter la spécialisation (duplication) et éviter l’usage de Object + casts (risque de ClassCastException à l’exécution) en gardant la sécurité de type à la compilation.
1. Le problème initial
Question : Quel est le problème initial ? Réponse : Sans généricité, on a deux mauvaises options :
- Duplication de code : Créer une classe spécifique pour chaque type (ex:
CellOfString,CellOfDate,CellOfInteger…). C’est lourd et difficile à maintenir. - Perte de sécurité de type : Utiliser
Object(ex:Cellqui contientObject). Cela oblige à faire des “casts” (transtypages) explicites comme(String) message.get(). Si on se trompe de type, le compilateur ne dit rien, et le programme plante à l’exécution (ClassCastException).
2. La solution : Classes Génériques (Cell<E>)
On utilise un paramètre de type (souvent noté E, T, etc.) pour définir que le type contenu varie.
Le paramètre (ex: E) est une “variable de type” utilisable comme un type normal dans la classe.
// Définition
public class Cell<E> {
private final E object;
// ...
public E get() { return object; }
}Utilisation :
Cell<String> message = new Cell<>("Bonne année ");
Cell<Date> date = new Cell<>(Date.today());
<span color="yellow_bg">Le </span><span color="yellow_bg">`<>`</span><span color="yellow_bg"> vide s’appelle le “diamant” (</span><span color="yellow_bg">diamond</span><span color="yellow_bg">) et permet d’inférer automatiquement les types dans un </span><span color="yellow_bg">`new`</span><span color="yellow_bg">.</span>- Plus besoin de cast explicite :
message.get()retourne directement unString. - Sécurité à la compilation :
message.set(123)provoquerait une erreur de compilation.
3. Plusieurs paramètres de type (Pair<F, S>)
On peut avoir plusieurs types génériques. Exemple avec une paire :
Pair<String, Date> pair = new Pair<>("Bonne année", Date.today());
System.out.println(pair.fst() + pair.snd().year());4. Méthodes Génériques
Une méthode peut avoir ses propres paramètres de type (indépendants de ceux de la classe): on parle de “méthode générique”.
Parfois, une méthode doit introduire son propre paramètre de type, indépendant de celui de la classe.
Exemple pairWith :
Si on veut créer une paire à partir d’une Cell<E>, le second élément de la paire (S) n’est pas connu par la classe Cell. Il faut le déclarer dans la méthode.
// <S> déclare le nouveau type générique pour cette méthode
public <S> Pair<E, S> pairWith(S snd) {
return new Pair<>(this.object, snd);
}5. Généricité et Types Primitifs
Les types génériques (<E>) ne fonctionnent qu’avec des objets (classes). On ne peut pas écrire Cell<int>.
Solution manuelle (Wrappers)
On peut créer une classe qui “emballe” le type primitif.
Question : Pourquoi on emploie les fonctions valueOf et intValue ici ?
Réponse : C’est le principe de l’encapsulation et des “Wrappers”.
valueOf(factory method) permet de contrôler la création de l’objet (et potentiellement de mettre en cache des instances, comme le faitIntegerpour les petites valeurs).intValueest l’accesseur pour récupérer la valeur primitive brute stockée dans l’objet.
record Int(int intValue) {
public static Int valueOf(int intValue) {
return new Int(intValue);
}
}
// Utilisation
Pair<String, Int> pair = message.pairWith(Int.valueOf(2026));Solution Java (Auto-boxing)
Java fournit déjà des classes “Wrapper” pour chaque type primitif (Integer pour int, Double pour double, etc.).
Le compilateur fait le travail automatiquement (Auto-boxing / Unboxing).
Rappel: les types primitifs ne peuvent pas être utilisés comme paramètres de type (donc Cell<int> est interdit).
// Le compilateur transforme 2026 en Integer.valueOf(2026)
Pair<String, Integer> pair = message.pairWith(2026);Piège de l’Auto-boxing
Attention à la comparaison avec ==.
Integer x = 2026;
Integer y = 2026;
System.out.println(x == y); // Affiche FALSE !Explication : x et y sont deux objets différents en mémoire. == compare les références (adresses mémoire), pas le contenu. Pour comparer les valeurs, il faut utiliser x.equals(y) ou (int)x == (int)y.
6. Bornes de la Généricité (Bounded Type Parameters)
On peut imposer une borne supérieure avec extends (ex: T extends Number) pour garantir l’accès aux méthodes de la classe borne (ex: doubleValue()).
Parfois, on veut restreindre les types qui peuvent être utilisés avec un paramètre générique. Par exemple, on peut vouloir une méthode qui ne fonctionne que pour des types qui sont des Number (comme Integer, Double, Float…).
On utilise le mot-clé extends.
Exemple : Une méthode qui calcule la somme des valeurs dans une liste de nombres.
// T doit être un sous-type de Number
public <T extends Number> double sum(List<T> numbers) {
double total = 0.0;
for (T number : numbers) {
total += number.doubleValue(); // On peut appeler doubleValue() car on sait que T est un Number
}
return total;
}- Sécurité : On ne peut pas appeler
sumavec uneList<String>. - Fonctionnalité : À l’intérieur de la méthode, on peut utiliser les méthodes du type borne (ici,
Number).
On peut aussi borner les types au niveau de la classe :
public class NumericCell<T extends Number> {
// ...
}7. Types bruts (Raw types) et limitations
Type brut = type générique utilisé sans paramètre (ex: List au lieu de List<E>). À éviter: ils existent seulement pour la compatibilité avec l’ancien code.
Règle: n’utilisez jamais les types bruts dans votre code.
Limitations (Java): (1) pas de new T[] (tableaux génériques), (2) pas de instanceof Cell<String>, (3) casts génériques non sûrs (warnings et comportement potentiellement incorrect), (4) pas d’exceptions génériques.
8. Wildcards (?)
Les wildcards sont utilisés pour rendre les méthodes plus flexibles, notamment quand on ne se soucie pas du type exact, mais plutôt de ses propriétés.
Il y a trois types de wildcards :
a. Unbounded Wildcard (?)
List<?> signifie “une liste de n’importe quel type”. C’est utile quand le type n’a pas d’importance.
Exemple : Une méthode qui affiche la taille d’une liste.
public void printSize(List<?> list) {
System.out.println(list.size());
}On peut appeler printSize avec List<String>, List<Integer>, etc.
Attention : On ne peut rien ajouter (sauf null) à une List<?> car le compilateur ne connaît pas le type et ne peut pas garantir la sécurité.
b. Upper Bounded Wildcard (? extends Type)
List<? extends Number> signifie “une liste de n’importe quel type qui est un sous-type de Number”.
C’est le principe de la covariance. Une List<Integer> peut être considérée comme une List<? extends Number>.
Quand l’utiliser ? Quand on veut lire des données d’une structure générique (Producer).
Exemple : La méthode sum réécrite avec un wildcard.
public double sum(List<? extends Number> numbers) {
double total = 0.0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}Cette version est plus flexible que la version avec <T extends Number> car elle peut être appelée avec une List<Integer>, List<Double>, etc. sans que l’appelant ait à se soucier du type exact.
c. Lower Bounded Wildcard (? super Type)
List<? super Integer> signifie “une liste de n’importe quel type qui est un super-type de Integer” (donc Integer, Number, ou Object).
C’est le principe de la contravariance.
Quand l’utiliser ? Quand on veut écrire des données dans une structure générique (Consumer).
Exemple : Une méthode qui ajoute des entiers à une liste.
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}On peut appeler cette méthode avec une List<Integer>, une List<Number>, ou une List<Object>. Dans tous les cas, il est sûr d’y ajouter un Integer.
PECS (Producer Extends, Consumer Super)
C’est un moyen mnémotechnique pour se souvenir quand utiliser extends et super :
- Producer Extends : Si vous lisez des objets d’une collection (
Producer), utilisez? extends T. - Consumer Super : Si vous écrivez des objets dans une collection (
Consumer), utilisez? super T.