1. Le problème des collections « brutes » (raw types)
Un peu d’histoire. Avant Java 5, toutes les collections étaient « omnivores ». Elles stockaient des objets de type Object, et le compilateur ne contrôlait pas ce que vous y mettiez. Vous voulez y mettre une chaîne ? Pas de problème. Un nombre ? Pourquoi pas. Un chat ? C’est possible aussi.
// Exemple de collections « brutes » (raw types), Java avant la version 5
List list = new ArrayList();
list.add("Bonjour");
list.add(42);
list.add(new Object());
Le problème apparaissait au moment de « récupérer » et d’utiliser la valeur :
String s = (String) list.get(0); // OK, c'est une chaîne
String s2 = (String) list.get(1); // BOUM ! ClassCastException
Le compilateur se tait, et à l’exécution vous obtenez une ClassCastException. C’est comme une boîte portant l’étiquette « pommes », dans laquelle se trouvent une tasse, une banane et un hérisson.
Pourquoi est-ce mauvais ?
- Les erreurs n’apparaissent qu’à l’exécution.
- Les types se mélangent : vous devez convertir (caster) manuellement les objets vers le type voulu.
- Le code est moins lisible et plus risqué.
La solution — les génériques
Les generics (génériques) sont un mécanisme permettant de créer des classes, interfaces et méthodes avec des paramètres de type. Autrement dit, vous dites à la collection : « Ne stocke que des chaînes », et le compilateur s’en assure strictement.
List<String> words = new ArrayList<>();
words.add("Bonjour");
words.add("Monde");
// words.add(42); // Erreur de compilation ! Impossible d'ajouter un int dans List<String>
Désormais, le compilateur n’autorisera rien d’autre que des chaînes dans la liste. L’erreur sera détectée avant le lancement du programme.
Idée principale des génériques :
Assurer la sûreté de types des collections (et pas seulement), pour que les erreurs soient détectées à la compilation et non à l’exécution.
2. Syntaxe des génériques : à quoi ça ressemble dans le code
Indiquer le type entre chevrons
Quand vous créez une collection, indiquez le type des éléments entre <> :
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // Erreur : impossible d'ajouter un nombre dans une liste de chaînes
String first = names.get(0); // Pas besoin de cast !
Classiques :
- List<String> — liste de chaînes
- List<Integer> — liste d’entiers
- Set<Double> — ensemble de nombres à virgule flottante
- Map<String, Integer> — clé String, valeur Integer
Pourquoi ne pas écrire simplement List ?
On peut, mais vous perdez tous les avantages des génériques, et le compilateur vous avertira :
List list = new ArrayList(); // raw type — déconseillé !
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // Bonjour, ClassCastException !
Le code Java moderne utilise toujours les génériques.
Opérateur diamant <>
Depuis Java 7, vous pouvez ne pas indiquer le type à droite s’il est clair d’après le contexte :
List<String> list = new ArrayList<>(); // Le compilateur déduira qu'il s'agit de <String>
3. Génériques pour différentes collections
Exemples pour List, Set, Map
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alice");
uniqueNames.add("Bob");
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 23);
ages.put("Bob", 31);
Exemple avec une classe personnalisée
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. Points utiles
Avantages des génériques
Sûreté de types. Le compilateur veille à ce que seules des valeurs du type attendu soient placées dans la collection.
Plus besoin de conversions de type. Avant : String s = (String) list.get(0);. Maintenant : String s = list.get(0);.
Code plus lisible et plus fiable. Moins de surprises à l’exécution.
Limitations des génériques
Impossible d’utiliser des types primitifs. Les génériques ne fonctionnent qu’avec des objets, pas avec des primitifs (int, double, boolean). Utilisez les classes enveloppes : Integer, Double, Boolean.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int est automatiquement converti en Integer (autoboxing)
En bref sur l’effacement de types (type erasure)
En Java, les génériques sont implémentés via l’effacement de types : après compilation, l’information sur les paramètres de type est effacée, et à l’exécution la JVM ne sait pas qu’il s’agissait d’un List<String> plutôt que d’un simple List. C’est fait pour la rétrocompatibilité.
Conséquence : impossible de vérifier un paramètre de type via instanceof avec un argument de type concret.
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // Erreur de compilation !
Tenter d’ajouter un élément d’un autre type — erreur de compilation
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // Erreur de compilation : incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Chat", 5);
// map.put(3, "Éléphant"); // Erreur : la clé doit être String, la valeur — Integer
Et c’est très bien : les erreurs sont détectées à la compilation.
Pas seulement les collections
Vous pouvez utiliser les génériques dans vos propres classes et méthodes. Par exemple, une « Boîte » universelle :
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Bonjour");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
Dans les collections, les génériques sont la norme, mais vous les rencontrerez aussi ailleurs, par exemple dans le Stream API et Optional.
5. Erreurs typiques avec les génériques
Erreur n° 1 : Utiliser des collections « brutes ». Une écriture du type List list = new ArrayList(); supprime la sûreté de types. Indiquez toujours des paramètres de type, par exemple List<String>.
Erreur n° 2 : Tenter d’utiliser des primitifs. On ne peut pas écrire List<int>, utilisez List<Integer>.
Erreur n° 3 : Conversion manuelle lors de la lecture depuis une collection. Si vous utilisez des génériques, la conversion (String) list.get(i) est inutile. Si vous en avez besoin, c’est qu’il y a eu une rupture de types quelque part.
Erreur n° 4 : S’attendre à ce que les paramètres de type soient disponibles à l’exécution. À cause de l’effacement de types, on ne peut pas les vérifier via instanceof comme pour List<String>.
Erreur n° 5 : Mélanger différents types dans une même collection. Si vous avez déclaré List<String>, n’essayez pas d’ajouter un Integer — le compilateur le refusera, et c’est une bonne chose.
GO TO FULL VERSION