CodeGym /Cours /JAVA 25 SELF /Introduction aux génériques

Introduction aux génériques

JAVA 25 SELF
Niveau 16 , Leçon 4
Disponible

1. Introduction

Imaginez que vous avez une boîte universelle dans laquelle on peut mettre tout et n’importe quoi : une pomme, un livre ou même un jouet. En Java, cette « boîte universelle » — c’est une classe qui stocke des données du type le plus général, Object. Ce type est la superclasse de toutes les autres classes en Java, donc on peut placer absolument n’importe quel objet dans une variable de type Object.

Maintenant, imaginez un entrepôt rempli de telles boîtes. Si les boîtes n’ont pas d’étiquettes, vous pouvez y mettre ce que vous voulez, mais au moment de sortir quelque chose — il faudra ouvrir la boîte et deviner ce qu’il y a dedans. Avec les génériques, la situation ressemble à un entrepôt avec des inscriptions soignées : « Uniquement des pommes », « Uniquement des livres », « Uniquement des outils ». Vous savez toujours ce qu’il y a dans chaque boîte et vous ne pourrez pas, par erreur, mettre un livre dans la boîte pour les pommes.

À première vue, stocker dans Object semble pratique : il n’est pas nécessaire de créer des classes distinctes pour différents types de données. Mais en pratique, cette « universalité » se retourne contre vous :

  1. Facile de se tromper. Vous pouvez, par inadvertance, mettre dans la boîte un objet différent de celui attendu.
  2. Le compilateur ne voit pas le problème. Il vous laissera simplement mettre n’importe quoi, parce que le type Object l’autorise.
  3. Il faut « déballer » à la main. Quand vous sortez un objet d’une telle boîte, il est à nouveau de type Object, et vous devez le convertir vous‑même au type voulu (cela s’appelle une conversion de type ou cast). Et si vous vous trompez de type, le programme s’arrêtera tout simplement avec une erreur !

Voyons cela sur un exemple simple.

class Box {
    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}

Utilisons maintenant cette boîte :

Box box = new Box();
box.set("Bonjour"); // On a mis une chaîne
String s = (String) box.get(); // On a récupéré la chaîne, tout va bien

box.set(123); // On a mis un nombre
// Le compilateur ne voit pas le problème…
String t = (String) box.get(); // Erreur pendant l’exécution du programme !

Comme vous le voyez, le compilateur a laissé passer sans broncher un code qui a finalement conduit à une erreur. Nous n’avons découvert le problème que lorsque le programme a démarré et a « planté ».

2. Solution — les génériques

Generics (génériques) sont un moyen de résoudre ce problème. C’est une syntaxe spéciale qui permet de lier une classe ou une méthode à un type de données précis dès la compilation. En d’autres termes, c’est comme une étiquette sur la boîte qui dit : « Dans cette boîte, il peut n’y avoir que des chaînes » ou « Dans cette boîte, il peut n’y avoir que des nombres ».

Ainsi, nous obtenons une sécurité de type : le compilateur ne vous laissera pas mettre dans la « boîte » un objet du mauvais type. Il vérifiera votre code et signalera l’erreur avant même l’exécution du programme.

La même classe Box, mais cette fois avec des génériques :

class Box<Type> {
    private Type value;

    public void set(Type value) {
        this.value = value;
    }

    public Type get() {
        return value;
    }
}

Ici, Type est un paramètre de type. C’est un nom conditionnel que nous choisissons nous‑mêmes (on utilise généralement T, E, K, V), et il signifie : « Quand nous créerons une Box, nous indiquerons avec quel type elle va travailler, et j’utiliserai ce type partout où Type est écrit dans le code. »

3. Utilisation des génériques

Quand nous créons un objet d’une classe générique, nous indiquons le type concret entre chevrons <...>.

// Nous avons créé une boîte qui ne fonctionne qu’avec des chaînes
Box<String> stringBox = new Box<>();
stringBox.set("Bonjour, le monde!"); // OK, nous avons mis une chaîne
String s = stringBox.get(); // Récupération de la chaîne sans conversion de type

stringBox.set(123); // Erreur de compilation ! Le compilateur ne le permettra pas.

Si nous créons une Box<Integer>, le compilateur veillera à ce qu’on n’y mette que des nombres :

// Nous avons créé une boîte qui ne fonctionne qu’avec des nombres
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK, nous avons mis un nombre
Integer number = intBox.get(); // Récupération du nombre sans conversion de type

intBox.set("Bonjour"); // Erreur de compilation !

Désormais, le compilateur sait précisément quel type de données doit se trouver dans chaque boîte et nous protège efficacement des erreurs.

4. Comment cela fonctionne avec des exemples

On peut utiliser les génériques non seulement dans les classes, mais aussi dans les méthodes. Cela permet d’écrire un code très souple et universel.

Exemple 1 — classe générique

Supposons que nous ayons besoin d’une classe Pair qui stocke deux objets du même type. Avec les génériques, cela ressemble à ceci :

class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

Utilisation :

Pair<String> greetings = new Pair<String>("Bonjour", "Monde");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());

Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());

Dans le premier cas, le compilateur est certain que greetings contient des chaînes, dans le second — des nombres.

Exemple 2 — méthode générique

Nous pouvons écrire une méthode qui fonctionne avec n’importe quel type de données.

class Utils {
    // <T> avant void indique que la méthode utilisera un paramètre de type T
    public static <T> void printTwice(T value) {
        System.out.println(value);
        System.out.println(value);
    }
}

Nous pouvons maintenant appeler cette méthode avec n’importe quel type de données, et elle se comportera de la même manière :

Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);

5. Avantages des génériques

Les génériques ne sont pas un simple sucre syntaxique, mais un outil puissant qui résout de vrais problèmes en développement. Examinons trois avantages clés.

Sécurité de type — c’est l’avantage principal des génériques. Sans eux, le compilateur ne peut pas vérifier si vous utilisez correctement les types de données dans vos classes et méthodes « universelles ». Des erreurs telles que la tentative de mettre une chaîne dans une « boîte pour nombres » ne seront détectées qu’à l’exécution, quand le programme « plantera » avec une exception ClassCastException. Avec les génériques, le compilateur surveille strictement les types. Il vérifie que vous mettez uniquement des chaînes dans une Box<String> et uniquement des nombres dans une Box<Integer>. Si vous tentez de faire quelque chose d’incorrect, il le signalera immédiatement, et vous pourrez corriger l’erreur avant le lancement du programme. Cela rend votre code bien plus fiable et prévisible.

Code plus propre (sans conversions de type inutiles). Souvenez‑vous à quoi ressemblait le code avec notre « boîte universelle » sans génériques : Box box = new Box(); box.set("Bonjour"); String s = (String) box.get(); Chaque fois que vous sortiez un objet de la boîte, vous deviez écrire (String), (Integer), etc. Avec les génériques, cette nécessité disparaît. Le compilateur sait déjà quel type se trouve à l’intérieur et le convertit pour vous automatiquement : Box<String> stringBox = new Box<>(); stringBox.set("Bonjour"); String s = stringBox.get(); Cela raccourcit le code et en améliore considérablement la lisibilité et le confort d’utilisation.

Flexibilité et réutilisation du code. Les génériques permettent de créer de véritables classes et méthodes universelles qui fonctionnent avec différents types de données sans perdre la sécurité de type. Par exemple, la classe Box<T> peut servir à stocker des chaînes, des nombres, vos propres classes (Student, Car) — n’importe quoi ! Vous n’avez pas besoin d’écrire des classes séparées StringBox, IntegerBox et StudentBox. Vous écrivez une seule classe universelle Box<T> et vous indiquez simplement le type requis lors de la création de l’objet. Cela réduit la quantité de code, évite les duplications et rend votre programme plus modulaire et flexible.

6. Limitations des génériques

Malgré tous leurs avantages, les génériques ont plusieurs limitations importantes qu’il faut connaître.

Types primitifs (primitives). Vous ne pouvez pas utiliser de types primitifs (tels que int, double, boolean, etc.) comme paramètre de type. Par exemple, le code Box<int> intBox = new Box<>(); provoquera une erreur de compilation. À la place, vous devez utiliser leurs « classes enveloppes » (Integer, Double, Boolean). Box<Integer> intBox = new Box<>();. Le compilateur Java sait convertir automatiquement les primitifs en classes enveloppes et inversement (int en Integer et retour) — ce mécanisme s’appelle autoboxing/unboxing.

Effacement des types (Type Erasure). C’est une caractéristique clé de l’implémentation des génériques en Java. L’idée est que le compilateur Java n’utilise l’information sur les génériques que pendant la compilation. Une fois qu’il a vérifié votre code et s’est assuré de sa sécurité de type, il efface toute information sur les paramètres de type. Par exemple, Box<String> et Box<Integer> dans le code compilé (bytecode) apparaîtront simplement comme Box. Cela signifie que, pour la machine virtuelle Java (JVM), ils deviennent un seul et même type.

Qu’est‑ce que cela signifie pour vous ? Vous ne pouvez pas créer de tableau générique, par exemple new Box<String>[10], et vous ne pouvez pas utiliser instanceof avec des génériques pour vérifier si un objet est instanceof Box<String>. C’est un sujet plus complexe, mais sa compréhension est importante pour une étude approfondie de Java. À ce stade, il suffit de retenir que les génériques sont, en substance, une « étiquette » pour le compilateur qui l’aide à vérifier le code, mais cette étiquette est retirée lorsque le programme est prêt à être exécuté.

1
Étude/Quiz
Classes imbriquées et internes, niveau 16, leçon 4
Indisponible
Classes imbriquées et internes
Classes imbriquées et internes
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION