Lumière sur les génériques – 2

Nous avions laissé nos héros au moment où ils allaient se rendre compte que List<Integer> n’était pas une extension de List<Number>. Emplis de désarroi, ils se tournèrent donc vers le type anonyme ?, qui va leur permettre de contourner ce problème. Même si ce chemin n’est certes pas exempt d’embûches, cela leur permettra au moins de terminer le tour d’horizon des génériques, commencé la semaine dernière.

Classes génériques et héritage

Avec cette partie, nous entrons dans l’un des points probablement les plus contre-intuitifs des génériques.

La première chose que l’on apprend (et enseigne) pour expliquer l’héritage est la relation est. Si un entier est un nombre, alors il est naturel que la classe Integer étende la classe Number. Donc, si une liste d’entiers est une liste de nombres, il semble naturel que la classe (ou plutôt ici l’interface) List<Integer> étende la classe List<Number>. Hélas pour la nature, il n’en est rien, ces deux interfaces ne s’étendent pas l’une l’autre, ni dans un sens, ni dans l’autre.

Examinons pourquoi les choses ont été faites comme ça. Écrire ce genre de code ne pose pas de problème.

Integer integer = new Integer(1) ;
Number number = integer ;

Effectivement, on peut toujours affecter une variable d’un certain type, dans une variable d’un de ses super-types.

Supposons que l’on puisse aussi écrire ce code.

List<Integer> integerList = new ArrayList<Integer>() ;
// Cette ligne ne compile pas, car List<Integer> n'étend pas List<Number> !!
List<Number> numberList = integerList ;
// Si cette affectation était légale, on pourrait écrire le code suivant
numberList.add(new Float(3.14)) ;

Ajouter un Float à une liste de Number est raisonnable. Mais si List<Integer> étend List<Number>, alors il est impossible de savoir dès la compilation, si l’implémentation de cette liste est en fait une liste de nombres ou d’entiers. Cet élément ne peut être connu qu’à l’exécution. C’est donc à l’exécution que l’erreur devrait apparaître, sous la forme d’une exception analogue à ArrayStoreException.

Plutôt que de mettre ce type de mécanisme en place, les concepteurs ont préféré ne pas créer cette relation d’héritage. Ce point peut devenir réellement pénible lorsque l’on utilise des paramètres génériques. Par exemple, la méthode suivante ne peut pas être appelée avec une liste de Integer :

public static void shuffle(List<Number> list) {
	// corps de la méthode
}

Type des arguments, ? et capture conversion

C’est en partant de ce constat que Java introduit le type wildcard ?. La JLS définit très précisément les règles de déclaration et d’utilisation des wildcards. Le type wildcard est déclaré par le caractère ?. Il ne peut être utilisé que pour qualifier le type générique d’un paramètre d’une méthode, aussi appelé type d’argument (traduction personnelle de argument type).

Il est possible de borner une wildcard, de deux façons :

  • ? extends Number : désignent tous les types qui étendent Number ;
  • ? super Integer : désignent tous les types de la hiérarchie Integer.

Le type List<T> étend-il le type List<?> ?

La JLS définit très précisément les règles d’héritage entre les types wildcards, même si ces types sont particuliers, dans la mesure où appeler getClass() sur leurs instances, ou utiliser instanceof dessus n’a pas vraiment le même sens que sur les classes  non paramétrées. Est-ce vraiment l’héritage qui permet d’expliquer la validité de l’exemple suivant ? Pour cela, il faudrait que List<?> étende List<T>.

public static void shuffle(List<?> list) {
	shuffle2(list) ;
}

private static <T> void shuffle2(List<T> list) {
	// corps de la méthode
}

Le mécanisme qui est fait mis en œuvre ici s’appelle capture conversion.

Qu’est-ce qu’une capture dans le jargon de la JLS (et de la programmation objet) ? Il s’agit simplement du type représenté par « ? » ou, si ? est borné, « ? extends Number« . Ce type n’a pas de nom, il est donc anonyme, et comme pour les classes anonymes, il se voit attribuer un nom par le compilateur.

Dans le cas de notre exemple, le compilateur se rend compte que la liste de la méthode shuffle() possède un type concret à l’exécution. Il capture donc ce type dans un type X (anonyme),  et il infère ensuite que X et T sont le même type. Cette conversion est sans risque, et est donc valide.

Les capture conversion sont résolues à la compilation, et non à l’exécution, ce qui explique qu’une erreur de capture conversion se traduit toujours pas une erreur de compilation.

Hiérarchie des types ?, bornés ou non

Encore une fois, la JLS vient à notre secours (quoique…) pour définir les règles d’héritage entre ces types. Ces règles sont précises, et définissent pour chaque type, son super-type immédiat. Par transitivité, on peut en déduire les hiérarchies complètes.

  • Le type raw (celui que l’on obtient après avoir appliqué le type erasure) reste le super-type de tous les types raw des sous-types définis. Par exemple, le type Collection (non générique) reste le super-type de Set, List et ArrayList. Cette règle permet de conserver la compatibilité avec les types non génériques, ce qui est un minimum.
  • Le type raw est le super-type de tous les types génériques possibles, qu’ils comportent une wildcard ou non. Par exemple, Collection est le super-type de Collection<Number>, Collection<Integer>, mais aussi de Collection<?> et Collection<? extends Number>.

  • Les relations d’héritage sont les mêmes pour les types génériques que pour les types raw, tant que le type générique ne varie pas dans la hiérarchie. Donc Collection<Number> est le super-type de List<Integer>, Set<Integer>, et ArrayList<Integer>. De même pour Collection<? extends Number> est le super-type de List<? extends Number>Set<? extends Number>, et ArrayList<? extends Number>. En revanche, aucune relation n’existe entre List<Number>, et List<? extends Number>, puisque le type générique n’est plus le même.

  • Le type générique construit sur le type ? est le super-type de tous les types génériques que l’on peut construire sur ce type, qu’ils comportent une wildcard ou non. Par exemple, le type Collection<?> est le super-type de Collection<Number> et Collection<? extends Number>.

  • Un type générique construit sur un type ? extends X (où X est un type concret donné) est le super-type du type générique construit sur X, de tous les types génériques construits sur les sous-types de X, et de tous les types génériques bornés par des extensions des sous-types de X. En clair, Collection<? extends Number> est le super-type de Collection<Float> et de Collection<? extends Float> (ce qui est un peu une vue de l’esprit, puisque Float est une classe final).

Des relations analogues existent aussi lorsque le type wildcard est borné par super.

  • Le type générique construit sur un type ? super X (où X est un type concret donné) est le super-type de tous les types ? super Y, où Y est un type concret, sous-type de X. Par exemple, List<? super Number> est le super-type de List<? super Serializable>.
  • Il est également super-type du type X et de tous les sous-types de X. Par exemple, List<? super Number> est le super-type de List<Number> et List<Serializable>.

La bonne utilisation des deux bornes extends et super est assez subtile. Cay Horstman et Gary Cornell donnent un exemple très pertinent dans Core Java.

Supposons que l’on veuille écrire une méthode statique capable de déterminer le plus petit élément d’un tableau. Les éléments de ce tableau doivent être Comparable, donc il est raisonnable d’imposer que le type générique sur lequel cette méthode est construite soit une extension de Comparable<T>. La déclaration de cette méthode peut être la suivante.

public static <T extends Comparable<T>> T min(T[] a) { ... }

Cette déclaration de méthode est-elle correcte ? En fait non. Les auteurs prennent l’exemple de la classe Calendar. La classe Calendar est étendue par GregorianCalendar. La classe Calendar implémente Comparable<Calendar>, ce qui fait que GregorianCalendar ne peut pas implémenter Comparable<GregorianCalendar>, mais implémente Comparable<Calendar>. La classe GregorianCalendar ne peut donc pas être passée en paramètre de cette méthode. Le problème est que Comparable<T> est une restriction trop restrictive, précisément. On peut modifier cette déclaration de la façon suivante.

public static <T extends Comparable<? super T>> T min(T[] a) { ... }

Cette fois, il est possible de passer GregorianCalendar en paramètre de cette méthode, puisque cette classe implémente Comparable<Calendar>, et que le type Calendar est bien super-type de GregorianCalendar. Même si cette déclaration peut paraître hermétique au premier abord, elle posera beaucoup moins de problèmes lors de son utilisation que sa première version.

Type ? et compatibilité

Les règles d’héritage permettent d’y voir un peu plus clair dans le maquis des règles de compatibilité entre type définis avec des wildcards.

La première chose qu’il faut comprendre, c’est qu’on ne peut pas créer de variable avec un type wildcard, borné ou non, à partir d’un new. Les deux seuls contextes dans lesquels on manipule une variable d’un tel type sont les suivants :

  • on se trouve dans une méthode, et cette variable est arrivée en tant que paramètre de cette méthode ;
  • on a fait un appel à une méthode, et cette variable est celle qui nous a été retournée par cette méthode.

La question de la compatibilité entre types se résume donc à trois cas:

  • on appelle une méthode en lui passant un type réel en paramètre ;
  • on appelle une méthode et l’on stocke le résultat qu’elle retourne dans une variable d’un type réel ;
  • on appelle une méthode en lui passant en paramètre la valeur retournée par une autre méthode (ou pourquoi pas la même).

Examinons l’exemple suivant.

Class<String> c = "Bonjour".getClass() ; // ne compile pas !!

Le message d’erreur fourni par le compilateur est assez cryptique :

Type mismatch: cannot convert from Class<capture#2-of ? extends String> to Class<String>

Effectivement, la méthode Object.getClass() retourne une variable dont le type est défini dans la javadoc.

The actual result type is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.

Comme indiqué, le type |X| représente l’erasure du type X, donc le type de X, auquel on a retiré toutes les références aux types génériques (cette notation est définie dans la JLS). Ce que nous dit la Javadoc, c’est que le type de retour de cette méthode pour la classe String est le type ? extends String. Ce type est donc une capture, au sens où l’on vient de le définir.

Ce que nous dit le compilateur, c’est que ce type capture, qui représente le type que l’on écrit ? extends String, n’est pas compatible avec le type String, et ne peut donc pas être converti dans ce type. Le code suivant, lui, compile correctement.

Class<? extends String> c = "Bonjour".getClass() ; // compile correctement

Comme il n’y a pas de relation entre les types Class<String> et Class<? extends String>, l’erreur de compilation apparaît.

Les règles de conversion autorisées sont bien sûr spécifiées dans la JLS, et sont compatibles avec les règles d’héritage que nous venons de donner.

Déterminons le nombre de cas à examiner. Nous avons quatre types possibles :

  • le type anonyme ? ;
  • le type ? extends T (anonyme aussi) ;
  • le type ? super T (également anonyme) ;
  • le type T.

Dans le type T, on peut distinguer trois types : le type Object, et deux types jouet : A et B, B étant une extension de A. Cela nous fait donc 36 affectations possibles. Parmi ces 36, neuf ne concernent pas les types génériques ; ce sont les affectations croisées entre Object, A et B.

Il nous reste donc 27 cas réellement génériques à traiter. Sur ces 27 cas restants, on peut encore en retirer 4, qui sont en doublon. Ce sont les cas où l’affectation de A ou de B est équivalente. Il nous reste en réalité 23 cas à traiter.

Nous avons besoin de quelques munitions pour examiner ces 23 cas. Reprenons une version modifiée de notre classe Holder<T>, qui au passage devient une interface.

public interface Holder<T> {

	public void processExtends(Holder<? extends T> holder) ;

	public void processSuper(Holder<? super T> holder) ;

	public Holder<?> anonHolder() ;

	public Holder<T> typeHolder() ;

	public Holder<? extends T> extendHolder() ;

	public Holder<? super T> superHolder() ;
}

Définissons les objets instances des types suivants.

Holder<?> anonHolder ;
Holder<Object> objectHolder ;
Holder<A> aHolder ;
Holder<B> bHolder ;

Affectation dans le type Holder<?>

Les cinq affectations dans ce type compilent.

// ces cinq affectations compilent correctement
anonHolder = holder.anonHolder() ;
anonHolder = holder.objectHolder() ;
anonHolder = holder.typeHolder() ;
anonHolder = holder.extendHolder() ;
anonHolder = holder.superHolder() ;

Affectation dans le type Holder<Object> ou Holder<A>

Aucun type wildcard ne peut être affecté dans Holder<Object>.

// aucune de ces affectations ne compile
// ni pour Object
objectHolder = holder.anonHolder() ;
objectHolder = holder.extendHolder() ;
objectHolder = holder.superHolder() ;
// et pas plus pour A
aHolder = holder.anonHolder() ;
aHolder = holder.extendHolder() ;
aHolder = holder.superHolder() ;

Affectation dans le type Holder<? extends A>

// ces trois premières affectation ne compilent pas
holder.processExtends(anonHolder) ;
holder.processExtends(objectHolder) ;
holder.processExtends(holder.superHolder()) ;
// ces trois dernières affectations compilent correctement
holder.processExtends(aHolder) ;
holder.processExtends(bHolder) ;
holder.processExtends(holder.extendHolder()) ;

On vérifie bien sur cet exemple que l’on peut affecter dans une variable de type ? extends A :

  • le type A, et les types qui l’étendent ;
  • le type ? extends A.

Affectation dans le type Holder<? super A>

// ces trois premières affectation ne compilent pas
holder.processSuper(anonHolder) ;
holder.processSuper(bHolder) ;
holder.processSuper(holder.extendHolder()) ;
// ces trois dernières affectations compilent correctement
holder.processSuper(objectHolder) ;
holder.processSuper(aHolder) ;
holder.processSuper(holder.superHolder()) ;

On vérifie bien sur ce dernier exemple que l’on peut affecter dans une variable de type ? super A :

  • le type générique Object ;
  • le type A lui-même, mais pas son type étendu B ;
  • le type ? super A.

On constate également que les comportements des types ? super A et ? extends A ne sont pas symétriques, ce qui est logique.

Les génériques et Java 7

Java 7 va apporter quelques modifications pour les génériques. L’opérateur dit « diamant » va être introduit, qui permettra d’alléger la création d’instances génériques. Le compilateur va devenir un peu plus intelligent qu’il n’est aujourd’hui, et autoriser ce genre d’écriture :

List<String> stringList = new ArrayList<>() ;

Dans le cas de cet exemple, le compilateur créera automatiquement une instance de ArrayList<String>.

Références

La JLS, chapitre 4 : http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html

La JSR 14 : http://www.jcp.org/aboutJava/communityprocess/review/jsr014/

Le tutorial sur le site de Sun, très léger : http://download.oracle.com/javase/tutorial/java/generics/index.html

La FAQ sur ce sujet de Angelika Langer : http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html

Le chapitre 12 de l’excellent Core Java, de Cay Horstmann & Gary Cornell : http://www.horstmann.com/corejava.html