Mécanisme d'effacement et limitations

Mécanisme d’effacement et limitations #

Le mécanisme d’effacement (appelé erasure en anglais) est la technique employée depuis Java 5 (2004) pour offrir les génériques. L’objectif était de rester compatible avec les anciennes versions de Java (dont les collections).

La transformation des déclarations génériques est réalisée à la compilation. A l’exécution, une instance ne préserve pas d’information quant aux types réels des paramètres. Nous verrons les limitations que cela implique.

Le mécanisme est le suivant:

  • suppression de la déclaration des paramètres <T>
  • remplacement des occurrences de T par le type Object
  • casting lors d’appels de méthodes

La déclaration de Box<T> simplifiée:

1
2
3
4
5
6
7
8
public class Box<T> {
  private T t;

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

  public void set(T t) { this.t = t; }
  public T get() { return this.t; }
}

et une utilisation quelconque:

1
2
Box<Integer> bi = new Box<>(10);
int i = bi.get(); 

seraient équivalentes, lors de la compilation, à les remplacer par:

1
2
3
4
5
6
7
8
public class Box {
  private Object t;

  public Box(Object t) { this.t = t; }

  public void set(Object t) { this.t = t; }
  public Object get() { return this.t; }
}

et:

1
2
Box bi = new Box(10);
int i = (Integer)bi.get();

Conséquences #

Cette stratégie est efficace, mais elle comporte des défauts :

  • les possibilités sont réduites
  • les règles sont parfois difficiles à appréhender

Différents extraits des limitations #

Paramètres primitifs impossibles #

1
2
Box<int> bi = new Box<>(2); // Erreur de compilation
Box<Integer> bi = new Box<>(2); // Ok, autoboxing de 2

Instanciation impossible #

Effectivement, il est impossible de déterminer si un paramètre est instanciable (s’il s’agit d’une classe et non d’une interface ou d’une classe abstraite) et s’il existe un constructeur sans arguments.

1
2
3
4
5
6
class Test<T> {
  public Test(){
    T t = new T(); // Erreur de compilation 
                   // (required class, found type param)
  }
}

Une solution reste cependant possible:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Test<T> {
  public Test(Class<T> cls) throws Exception {
    T t = cls.newInstance(); 
    /* si E a un constructeur sans argument, 
     * sinon InstantiationException */
  }
}
...
Test<String> t = new Test<>(String.class); // ok
Test<Integer> t = new Test<>(Integer.class); // KO, Integer n'a pas de constr. sans arg.

Perte du paramètre à l’exécution #

1
2
3
4
5
6
7
8
Box<Integer> bi = new Box<>(1);
Box<Double> bd = new Box<>(1.0);

bi instanceof Box // vaut true
bd instanceof Box // vaut true
bi instanceof Box<Integer> // Interdit

bi.getClass() // retourne une classe Box

Ce qui rend parfois compliquée une redéfinition de la méthode equals avec des types génériques.

Surcharge impossible #

Une autre conséquence est de rendre la surcharge impossible avec les génériques:

1
2
3
4
5
6
7
class Util {
  void take(List<Integer> xs) { ... }
  void take(List<String> xs) { ... }
}
|  Error:
|  name clash: take(java.util.List<java.lang.String>)
|   and take(java.util.List<java.lang.Integer>) have the same erasure

Le Raw type #

Lorsqu’un type générique est déclaré sans ses paramètres, on l’appelle un raw type (traduisez pas type brut). Il est permis pour autoriser la compatibilité avec les anciennes versions de Java qui ne supportaient pas encore les génériques.

1
2
3
List os = new ArrayList();
os.add("hello"); // warning: unchecked call to add(E) ...
os.add(42); // warning: unchecked call to add(E) ...

Evitez-les. Indiquez les paramètres entre chevrons.

1
List<Integer> is = new ArrayList<>();

Le danger est de permettre ce genre de bug:

1
2
3
4
5
6
7
Box<Integer> bi = new Box<>(10); // ~> Box bi = new Box(10)
Box b = bi; // raw type (bi référence le même objet que bi)
b.set("Coucou"); // Warning du compilateur mais Ok! 
                 // (on remplace 10 dans notre boîte d'entiers par 
                 // une chaîne de caractères)
Integer i = bi.get(); // ClassCastException au runtime 
                      // (l'erreur survient lors de la récupération, à l'exécution)

Conclusion #

Ce type d’erreur est inhérent à la JVM et au bytecode Java. Ces limitations peuvent se rencontrer dans d’autres langages supportés par la JVM comme Scala ou Kotlin. Cependant, ces derniers offrent des mécanismes pour renforcer le système de typage en offrant une syntaxe permettant de connaître les paramètres d’un générique à l’exécution. Kotlin utilise les reified type parameters et Scala peut le faire à l’aide des TypeTag ou ClassTag implicites.

Approfondissement des restrictions en Java : docs.oracle.com .












comments powered by Disqus