Les types génériques #
Terminologie #
Prenons l’exemple des listes pour comprendre la terminologie.
List<E>
est un type générique.E
est un type paramétrique ou un paramètre du typeList
.- Une fois le ou les paramètres renseignés, un type générique devient un type concret:
List<Integer>
est un type concret, il s’agit d’une liste d’entiers. Il ne s’agit plus d’un type générique.
Il s’agit du dernier type de polymorphisme: le polymorphisme paramétrique.
Utilité #
La principale utilité, vous l’aurez compris, est de réduire la duplication de code. Une autre utilité, et pas des moindres, est de bénéficier d’un code plus sûr. Prenons l’exemple de List
avant l’usage des génériques (versions de Java antérieur à 5).
Sans générique, Java utilisait des Object
pour utiliser n’importe quel type.
|
|
La dernière ligne est problématique et obligeait l’utilisateur à réaliser une conversion. Il devenait même important de vérifier la référence au préalable:
|
|
Grâce aux génériques, une fois le paramètre spécifié, le code devient plus robuste. La méthode get
retourne le type spécifié et le cast devient inutile.
|
|
L’interface de List
permet de bien comprendre le mécanisme:
interface List<E> {
boolean add(E e);
E get(int index);
E remove(int index);
E set(int index, E element);
int size();
...
}
Pour une liste de String
's, il devient possible d’interpréter, par substitution, l’interface ainsi :
interface List<String> {
boolean add(String e);
String get(int index);
String remove(int index);
String set(int index, String element);
int size();
...
}
Raw type #
Les génériques ont été introduits avec Java 5. L’objectif était de rester compatibles avec les anciennes versions. Il est donc toujours possible d’utiliser une telle syntaxe sans les crochets, sans les paramètres :
List rawType = new ArrayList();
Un raw type
(traduisez par “type brut”) est un type générique sans paramètre. Des alertes sont cependant levées à la compilation lors de manipulation dangereuse:
List rawType = new ArrayList();
List<String> strings = rawType; // warning: unchecked conversion
La référence rawType
peut contenir n’importe quel objet. Du coup, récupérer un élément de strings
ne retourne pas forcément un String
!
List<String> strings = List.of("hello");
List rawType = strings;
rawType.add(42); // warning: unchecked call to add(E) ...
Dans l’exemple ci-dessus, rawType
référence une liste de String
mais peut ajouter n’importe quel type d’objets, ce qui viole le contrat de strings
. Un warning est généré à la compilation et une exception serait levée lors de l’exécution.
Mise en oeuvre d’une Box
#
Réalisons maintenant notre propre classe générique selon le cahier des charges ci-dessous:
- Une boîte
Box
peut contenir une valeur de n’importe quel type - Il est possible de récupérer sa valeur ou de modifier sa valeur
- Elle redéfinit la méthode
toString()
,equals()
ethashCode()
- Il ne doit pas être possible d’hériter de
Box
|
|
La ligne 24 utilise le jocker pour convertir la référence en une Box
. La ligne 25 utilise le equals
de l’objet qui se trouve dans la boîte.
Voici un exemple d’utilisation:
|
|
Affichage:
[1]
[2]
false
[3]
1
Méthode d’instance générique #
Ajoutons une fonction permettant de transformer une boîte ; appelons-la map
. Nous souhaitons par exemple transformer une boîte contenant un entier en une boîte contenant une chaîne de caractères : Box<Integer> -> Box<String>
. Cette méthode retourne une nouvelle boîte.
Par exemple:
|
|
L’extrait de code ci-dessus doit afficher:
[La boite contient: 42 !]
Voici la méthode à ajouter à notre Box
.
// (Box<T>, T -> R) -> Box<R>
public <R> Box<R> map(Function<T, R> f) {
/* ^
* |
* \- - - - permet de retourner une boîte d'un autre type */
return new Box<R>(f.apply(this.t));
}
Cette méthode est générique, car sans la déclaration du public <R> Box...
, le compilateur rechercherait un type R
existant.
Voici la déclaration complète de la classe avec cette nouvelle méthode. Réalisez le TODO
pour comprendre pourquoi cette méthode doit être générique.
|
|
Méthode statique générique #
Dans cet exemple, il est important de déclarer le type T
comme étant un paramètre. Si nous l’omettons, le compilateur chercherait un type T
connu. La méthode areEquals
permet ici de contraindre les boîtes à comparer à avoir le même type.
|
|
Exemple d’utilisation:
|
|
Paramètres bornés #
Un paramètre peut être borné lors de sa déclaration. C’est-à-dire qu’il est possible de préciser qu’un paramètre doit être d’un type ou d’un sous-type particulier. Imaginons que nous souhaitions créer uniquement des boîtes numériques. Une borne se déclare à l’aide du mot-clé extends
.
|
|
Un avantage est de pouvoir appeler toutes les méthodes spécifiques à cette borne.
Voyons maintenant un exemple d’une méthode générique bornée (en l’occurrence statique):
|
|
Conventions #
La borne peut être une classe ou une interface. Il est possible de contraindre le paramètre à plusieurs types:
<T extends I1 & I2 & I3> // T doit respecter plusieurs interfaces
<T extends C & I1 & I2> // T doit être une classe C ou hériter de C
// et respecter les interfaces I1 et I2
Si une des bornes est une classe, elle doit être placée en premier.
Nommage des paramètres #
Le paramètre, en Java, est généralement une lettre indiquant parfois la nature de celui-ci:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
Exercice - l’interface Comparable
#
L’interface Comparable<T>
indique qu’il est possible de comparer un type. Elle oblige à redéfinir la méthode int compareTo(T o)
La classe Integer
hérite de Comparable<Integer>
; la classe String hérite quant à elle de Comparable<String>
:
public final class Integer implements Comparable<Integer> ... {
...
public int compareTo(Integer anotherInteger) { ... }
}
public final class String implements Comparable<String> ... {
...
public int compareTo(String anotherString) { ... }
}
Modifiez la classe Box<T>
pour permettre de comparer deux boîtes:
- Une
Box
doit respecter l’interfaceComparable
- Pour comparer des
Box
s, il suffit de comparer la valeur qu’ils encapsulent. Le paramètre est donc lui aussi comparable ! - Créer une méthode statique utilitaire pour préciser si une
Box
est plus grande qu’une autre- par ex.:
|
|