APIs cœur - Génériques
Comprendre les génériques Java avec paramètres de type, wildcards extends/super et méthodes génériques pour un code plus sûr et réutilisable.
Pourquoi cette étape est importante
Les génériques sont l'un des outils les plus importants de Java pour écrire du code sûr et réutilisable.
Sans génériques, les collections et APIs utilisent Object, ce qui crée des erreurs de cast à l'exécution.
Avec les génériques, les erreurs sont détectées dès la compilation.
Le problème que résolvent les génériques
Avant les génériques, on pouvait mettre n'importe quoi dans une liste :
List raw = new ArrayList();
raw.add("hello");
raw.add(42);
Ça compile, mais les erreurs de type arrivent plus tard à la lecture.
Avec les génériques :
List<String> names = new ArrayList<>();
names.add("briac");
// names.add(42); // erreur de compilation
Java impose alors le type attendu.
Paramètres de type
Un paramètre de type est un type “placeholder” (T, E, K, V) défini sur des classes, interfaces ou méthodes.
Conventions courantes :
T-> type génériqueE-> type d'élément (collections)K,V-> clé/valeur pour les maps
Exemple avec une classe générique :
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
Utilisation :
Box<String> textBox = new Box<>();
textBox.set("hello");
String value = textBox.get();
Paramètres bornés
Parfois, il faut limiter les types autorisés.
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public double toDouble() {
return value.doubleValue();
}
}
T extends Number signifie que seuls les types numériques sont permis (Integer, Double, etc.).
Wildcards : ? extends et ? super
Les wildcards rendent les APIs flexibles quand les types exacts diffèrent.
? extends T (producteur)
À utiliser quand on lit des valeurs en T.
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
On peut passer List<Integer>, List<Double>, etc.
Mais on ne peut en général pas ajouter de nouvelles valeurs (sauf null), car le sous-type concret est inconnu.
? super T (consommateur)
À utiliser quand on écrit des valeurs de type T.
public static void addDefaults(List<? super Integer> target) {
target.add(0);
target.add(1);
}
On peut passer List<Integer>, List<Number> ou List<Object>.
Règle rapide : PECS
- Producer ->
extends - Consumer ->
super
Méthodes génériques
Une méthode peut définir ses propres paramètres de type, indépendamment de la classe.
public static <T> T first(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("list is empty");
}
return list.get(0);
}
Utilisation :
String firstName = first(List.of("alice", "bob"));
Integer firstNumber = first(List.of(10, 20));
On peut aussi borner les méthodes génériques :
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
Et l'effacement de type (type erasure) ?
Les génériques Java utilisent l'effacement de type :
- les infos de type sont en grande partie supprimées à l'exécution
List<String>etList<Integer>sont toutes deuxListau runtime
Conséquences pratiques :
- impossible de faire
new T() - impossible d'utiliser
instanceof List<String> - quand il faut le type runtime, passer
Class<T>
Exemple :
public static <T> T create(Class<T> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
Erreurs fréquentes à éviter
- utiliser des types bruts (
Listau lieu deList<String>) - sur-utiliser
<?>et perdre les informations de type utiles - confondre
List<Number>etList<Integer>(pas de relation parent/enfant) - forcer des casts non sûrs qui annulent le bénéfice des génériques
Mini exemple backend
public record ApiResponse<T>(boolean success, T data, String error) {}
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, null, message);
}
Utilisation :
ApiResponse<String> r1 = ok("created");
ApiResponse<Integer> r2 = ok(201);
ApiResponse<Void> r3 = fail("invalid token");
Un seul modèle, fortement typé pour plusieurs formats de payload.
À retenir
Pour bien utiliser les génériques Java :
- utiliser les paramètres de type pour la sûreté à la compilation
- appliquer des bornes quand l'API impose des contraintes
- utiliser
? extendset? superavec la logique PECS - utiliser les méthodes génériques pour rendre les utilitaires réutilisables
- éviter les types bruts et les casts non sûrs