Les types génériques

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 type List.
  • 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.

1
2
3
4
5
6
7
List ls = new ArrayList(); // équivaut à ArrayList<Object>
ls.add("Hello");
Object o = ls.get(0); 
/* ne compile pas. get() retournait un Object
String s = ls.get(0); 
*/
String s = (String)o; // Cast /!\

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:

1
2
3
4
5
6
List ls = new ArrayList(); 
ls.add("Hello");
Object o = ls.get(0); 
if (o instanceof String) {
  String s = (String)o;
}

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.

1
2
3
List<String> ls = new ArrayList<>();
ls.add("Hello");
String s = ls.get(0);

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() et hashCode()
  • Il ne doit pas être possible d’hériter de Box
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Objects;

public final class Box<T> {

  private T value;

  public Box(T value) { this.value = value; }

  public T get() { return this.value; }

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

  @Override
  public String toString() { return "[" + this.value.toString() + "]"; }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || o.getClass() != this.getClass()) {
      return false;
    }
    Box<?> other = (Box<?>)o;
    return this.value.equals(other.value);
  }

  @Override
  public int hashCode() {
    return Objects.hash(this.value);
  }
}

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:

1
2
3
4
5
6
7
8
Box<Integer> box1 = new Box<Integer>(1);
Box<Integer> box2 = new Box<>(2); // Diamond : Le type est inféré

System.out.println( box1 );
System.out.println( box2 );
System.out.println( box1.equals(box2) );
System.out.println( new Box<>(3) );
System.out.println( box1.get() );

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:

1
2
3
Box<Integer> bi = Box<>(42);
Box<String> bs = bi.map( i -> "La boite contient: " + String.valueOf(i) + " !" );
System.out.println(bs);

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.Objects;
import java.util.function.Function;

public final class Box<T> {

  private T value;

  public Box(T value) { this.value = value; }

  public T get() { return this.value; }

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

  @Override
  public String toString() { return "[" + this.value.toString() + "]"; }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || o.getClass() != this.getClass()) {
      return false;
    }
    Box<?> other = (Box<?>)o;
    return this.value.equals(other.value);
  }

  /* TODO:
   *      /- - - enlever le <R> ici et compilez !
   *      |
   *      v                                 */
  public <R> Box<R> map(Function<T, R> f) {
    return new Box<R>(f.apply(this.value));
  }

  @Override
  public int hashCode() {
    return Objects.hash(this.value);
  }
}

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.

1
2
3
4
5
class Util {
  public static <T> boolean areEquals(Box<T> b1, Box<T> b2) { 
    return b1.equals(b2);
  }
}

Exemple d’utilisation:

1
2
3
4
5
6
7
Util.<Integer>areEquals(new Box<>(10), new Box<>(10)); // ==> true

Util.<Integer>areEquals(new Box<>(10.0), new Box<>(10)); // ==> Erreur de compilation, la premire boîte n'est pas une Box<Integer>

Util.areEquals(new Box<>(10.0), new Box<>(10)); // ==> false (compile et s'exécute)
// en l'occurrence, le paramètre T serait inféré 
// comme Object (ou plus probablement Serializable)

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.

1
2
3
4
5
6
7
8
public final class Box<T extends Number> { 
  ... 
}

...
Box<Integer> bi = ... // ok, Integer hérite de Number
Box<Double> bd = ... // ok, Double hérite de Number
Box<String> bs = ... // erreur de compilation, String n'est pas un Number

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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Util {
  public static <T extends Number> Box<T> toBox(T v) {
    return new Box<T>(v);
  }
}

...
Box<Integer> bi = toBox(22); // ==> ok
Box<Integer> bi = toBox(22.0); // ==> ERROR:
    // incompatible types: inference variable T has incompatible bounds
Box<Double> bd = toBox(22.0); // ok

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’interface Comparable
  • Pour comparer des Boxs, 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.:
1
2
3
Box<Integer> b1 = new Box<>(1);
Box<Integer> b2 = new Box<>(2);
Util.isBigger(b1, b2); // doit retourner false 











comments powered by Disqus