Les bases des génériques Java

1. Introduction

Les génériques Java ont été introduits dans JDK 5.0 dans le but de réduire les bogues et d’ajouter une couche supplémentaire d’abstraction par rapport aux types.

Cet article est une introduction rapide à Generics in Java, son objectif et la manière dont ils peuvent être utilisés pour améliorer la qualité de notre code.

2. Le besoin de génériques

Imaginons un scénario dans lequel nous souhaitons créer une liste en Java pour stocker Integer ; on peut être tenté d’écrire:

List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();

Étonnamment, le compilateur va se plaindre de la dernière ligne. Il ne sait pas quel type de données est renvoyé. Le compilateur nécessitera un casting explicite:

Integer i = (Integer) list.iterator.next();

Aucun contrat ne peut garantir que le type de retour de la liste est un Integer. La liste définie peut contenir n’importe quel objet. Nous savons seulement que nous récupérons une liste en inspectant le contexte. Lorsque vous examinez des types, il ne peut que garantir qu’il s’agisse d’un Objet ; par conséquent, une conversion explicite est nécessaire pour garantir la sécurité du type.

Cette distribution peut être agaçante, nous savons que le type de données dans cette liste est un Entier. Les acteurs encombrent également notre code. Cela peut provoquer des erreurs d’exécution liées au type si un programmeur commet une erreur avec le transtypage explicite.

Ce serait beaucoup plus facile si les programmeurs pouvaient exprimer leur intention d’utiliser des types spécifiques et le compilateur pouvait s’assurer de l’exactitude de ce type. C’est l’idée de base derrière les génériques.

Modifions la première ligne de l’extrait de code précédent en:

List<Integer> list = new LinkedList<>();

En ajoutant l’opérateur diamant <> contenant le type, nous restreignons la spécialisation de cette liste au type Integer , c’est-à-dire que nous spécifions le type qui sera conservé dans la liste. Le compilateur peut appliquer le type lors de la compilation.

Dans les petits programmes, cela peut sembler un ajout trivial, mais dans les grands programmes, cela peut ajouter une robustesse significative et faciliter la lecture du programme.

3. Méthodes génériques

Les méthodes génériques sont les méthodes écrites avec une déclaration de méthode unique et pouvant être appelées avec des arguments de types différents. Le compilateur s’assurera de l’exactitude du type utilisé. Voici quelques propriétés des méthodes génériques:

  • Les méthodes génériques ont un paramètre de type (l’opérateur de losange englobant

le type) avant le type de retour de la déclaration de méthode ** Les paramètres de type peuvent être liés (les limites sont expliquées plus tard dans

article) ** Les méthodes génériques peuvent avoir différents paramètres de type séparés par des virgules

dans la signature de la méthode ** Le corps d’une méthode générique ressemble à une méthode normale

Exemple de définition d’une méthode générique pour convertir un tableau en liste:

public <T> List<T> fromArrayToList(T[]a) {
    return Arrays.stream(a).collect(Collectors.toList());
}

Dans l’exemple précédent, le <T> dans la signature de la méthode implique que celle-ci traite le type générique T . Cela est nécessaire même si la méthode renvoie vide.

Comme mentionné ci-dessus, la méthode peut traiter de plusieurs types génériques. Dans ce cas, tous les types génériques doivent être ajoutés à la signature de la méthode. Par exemple, si vous souhaitez modifier la méthode ci-dessus pour traiter le type T et le type. G , cela devrait être écrit comme ceci:

public static <T, G> List<G> fromArrayToList(T[]a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

Nous passons une fonction qui convertit un tableau avec les éléments de type T en une liste avec des éléments de type G. Un exemple serait de convertir Integer en sa représentation String :

@Test
public void givenArrayOfIntegers__thanListOfStringReturnedOK() {
    Integer[]intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);

    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

Il est à noter que la recommandation d’Oracle est d’utiliser une lettre majuscule pour représenter un type générique et de choisir une lettre plus descriptive pour représenter des types formels, par exemple dans Java Collections, T est utilisé pour le type, K pour la clé, V pour la valeur.

3.1. Génériques délimités

Comme mentionné précédemment, les paramètres de type peuvent être liés. Bounded signifie “ restricted “, nous pouvons restreindre les types pouvant être acceptés par une méthode.

Par exemple, nous pouvons spécifier qu’une méthode accepte un type et toutes ses sous-classes (borne supérieure) ou un type toutes ses super-classes (borne inférieure).

Pour déclarer un type de limite supérieure, nous utilisons le mot clé extends après le type suivi de la limite supérieure que nous souhaitons utiliser. Par exemple:

public <T extends Number> List<T> fromArrayToList(T[]a) {
    ...
}

Le mot clé extends est utilisé ici pour signifier que le type T étend la limite supérieure dans le cas d’une classe ou implémente une limite supérieure dans le cas d’une interface. Un type peut également avoir plusieurs limites supérieures, comme suit:

<T extends Number & Comparable>

Si l’un des types étendus par T est une classe (i.e Number ), il doit être placé en premier dans la liste des limites, sinon une erreur de compilation se produira.

4. Utilisation de caractères génériques avec des génériques

Les caractères génériques sont représentés par le point d’interrogation en Java « ? » Et ils sont utilisés pour faire référence à un type inconnu. Les caractères génériques sont particulièrement utiles lors de l’utilisation de génériques et peuvent être utilisés en tant que type de paramètre, mais tout d’abord, il convient de prendre en compte une note important .

  • On sait que Object est le supertype de toutes les classes Java, cependant, une collection de Object n’est pas le supertype d’une collection. **

Par exemple, List <Object> n’est pas le supertype de List <String> et l’affectation d’une variable de type List <Object> à une variable de type List <String> provoquera une erreur du compilateur. Cela permet d’éviter d’éventuels conflits si nous ajoutons des types hétérogènes à la même collection.

La règle Same s’applique à toute collection d’un type et à ses sous-types.

Considérons cet exemple:

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

Si nous imaginons un sous-type de Building , par exemple un House , nous ne pouvons pas utiliser cette méthode avec une liste de House , même si House est un sous-type de Building . Si nous devons utiliser cette méthode avec le type Building et tous ses sous-types, le caractère générique lié peut faire la magie:

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

Maintenant, cette méthode fonctionnera avec le type Building et tous ses sous-types.

Cela s’appelle un caractère générique de limite supérieure où type Building est la limite supérieure.

Les caractères génériques peuvent également être spécifiés avec une limite inférieure, où le type inconnu doit être un supertype du type spécifié. Les limites inférieures peuvent être spécifiées à l’aide du mot clé super suivi du type spécifique, par exemple, <? super T> signifie un type inconnu qui est une super-classe de T (= T et tous ses parents).

5. Type d’effacement

Les génériques ont été ajoutés à Java pour garantir la sécurité des types et pour s’assurer que les génériques ne causent pas de surcharge au moment de l’exécution, le compilateur applique un processus appelé type erasure sur les génériques au moment de la compilation.

Type erasure supprime tous les paramètres de type et les remplace par leurs limites ou par Object si le paramètre de type est illimité. Ainsi, le bytecode après compilation ne contient que des classes, interfaces et méthodes normales, garantissant ainsi qu’aucun nouveau type n’est produit. Le transtypage correct est également appliqué au type Object au moment de la compilation.

Voici un exemple d’effacement de type:

public <T> List<T> genericMethod(T t) {
    return list.stream().collect(Collectors.toList());
}

Une fois compilé, le type non lié T est remplacé par Object comme suit:

public List<Object> fromArrayToList(Object a) {
    return list.stream().collect(Collectors.toList());
}

si le type est lié, alors le type sera remplacé par le lien lors de la compilation, comme ceci:

public <T extends Building> void genericMethod(T t) {
    ...
}

changerait après la compilation:

public void genericMethod(Building t) {
    ...
}

6. Conclusion

Les génériques Java constituent un complément puissant au langage Java car ils facilitent le travail du programmeur et le rendent moins sujet aux erreurs. Les génériques imposent la correction du type au moment de la compilation et permettent surtout de mettre en œuvre des algorithmes génériques sans causer de surcharge aux applications.

Le code source qui accompagne l’article est disponible à l’adresse over sur GitHub .