Dépendance de type en Java, partie 2

Comprendre la compatibilité des types est fondamental pour écrire de bons programmes Java, mais l'interaction des écarts entre les éléments du langage Java peut sembler très académique aux non-initiés. Cet article en deux parties s'adresse aux développeurs de logiciels prêts à relever le défi! La partie 1 a révélé les relations covariantes et contravariantes entre les éléments plus simples tels que les types de tableaux et les types génériques, ainsi que l'élément spécial du langage Java, le caractère générique. La partie 2 explore la dépendance de type dans l'API Java Collections, dans les génériques et dans les expressions lambda.

Nous allons intervenir immédiatement, donc si vous n'avez pas déjà lu la partie 1, je vous recommande de commencer par là.

Exemples d'API pour la contravariance

Pour notre premier exemple, considérons la Comparatorversion de java.util.Collections.sort(), à partir de l'API Java Collections. La signature de cette méthode est:

  void sort(List list, Comparator c) 

La sort()méthode trie tout List. Il est généralement plus facile d'utiliser la version surchargée, avec la signature:

 sort(List
    
     ) 
    

Dans ce cas, extends Comparableexprime que le ne sort()peut être appelé que si les éléments nécessaires de comparaison de méthodes (à savoir compareTo)ont été définis dans le type d'élément (ou dans son supertype, grâce à :? super T)

 sort(integerList); // Integer implements Comparable sort(customerList); // works only if Customer implements Comparable 

Utilisation de génériques à des fins de comparaison

Évidemment, une liste n'est triable que si ses éléments peuvent être comparés entre eux. La comparaison se fait par la méthode unique compareTo, qui appartient à l'interface Comparable. Vous devez implémenter compareTodans la classe d'élément.

Ce type d'élément peut cependant être trié d'une seule manière. Par exemple, vous pouvez trier un Customerpar son identifiant, mais pas par anniversaire ou code postal. L'utilisation de la Comparatorversion de sort()est plus flexible:

 publicstatic  void sort(List list, Comparator c) 

Maintenant, nous comparons les éléments non pas dans la classe de l'élément, mais dans un Comparatorobjet supplémentaire . Cette interface générique a une méthode objet:

 int compare(T o1, T o2); 

Paramètres contradictoires

L'instanciation d'un objet plusieurs fois vous permet de trier les objets en utilisant différents critères. Mais avons-nous vraiment besoin d'un Comparatorparamètre de type aussi compliqué ? Dans la plupart des cas, Comparatorcela suffirait. Nous pourrions utiliser sa compare()méthode pour comparer deux éléments quelconques de l' Listobjet, comme suit:

class DateComparator implémente Comparator {public int compare (Date d1, Date d2) {return ...} // compare les deux objets Date} List dateList = ...; // Liste des objets Date sort (dateList, new DateComparator ()); // trie dateList

Collection.sort()Cependant, l'utilisation de la version plus compliquée de la méthode nous a mis en place pour des cas d'utilisation supplémentaires. Le paramètre de type contravariant de Comparablepermet de trier une liste de type List, car java.util.Dateest un supertype de java.sql.Date:

 List sqlList = ... ; sort(sqlList, new DateComparator()); 

Si nous omettons la contravariance dans la sort()signature (en utilisant uniquement ou non spécifié, unsafe ), alors le compilateur rejette la dernière ligne comme erreur de type.

Afin d'appeler

 sort(sqlList, new SqlDateComparator()); 

vous devrez écrire une classe supplémentaire sans particularités:

 class SqlDateComparator extends DateComparator {} 

Méthodes supplémentaires

Collections.sort()n'est pas la seule méthode API Java Collections équipée d'un paramètre contravariant. Des méthodes telles que addAll(), binarySearch(), copy(), fill(), etc., peuvent être utilisés avec la même souplesse.

Collectionsméthodes comme max()et min()offrent des types de résultats contravariants:

 public static 
    
      T max( Collection collection) { ... } 
    

Comme vous le voyez ici, un paramètre de type peut être demandé pour satisfaire plusieurs conditions, simplement en utilisant &. Le extends Objectpeut paraître superflu, mais il stipule qu'il max()renvoie un résultat de type Objectet non de ligne Comparabledans le bytecode. (Il n'y a pas de paramètres de type dans le bytecode.)

La version surchargée de max()with Comparatorest encore plus amusante:

 public static  T max(Collection collection, Comparator comp) 

This max() has both contravariant and covariant type parameters. While the elements of the Collection must be of (possibly different) subtypes of a certain (not explicitly given) type, Comparator must be instantiated for a supertype of the same type. Much is required of the compiler's inference algorithm, in order to differentiate this in-between type from a call like this one:

 Collection collection = ... ; Comparator comparator = ... ; max(collection, comparator); 

Boxed binding of type parameters

As our last example of type dependency and variance in the Java Collections API, let's reconsider the signature of the sort() with Comparable. Note that it uses both extends and super, which are boxed:

 static 
    
      void sort(List list) { ... } 
    

In this case, we're not as interested in the compatibility of references as we are in binding the instantiation. This instance of the sort() method sorts a list object with elements of a class implementing Comparable. In most cases, sorting would work without in the method's signature:

 sort(dateList); // java.util.Date implements Comparable sort(sqlList); // java.sql.Date implements Comparable 

The lower bound of the type parameter allows additional flexibility, however. Comparable doesn't necessarily need to be implemented in the element class; it's enough to have implemented it in the superclass. For example:

 class SuperClass implements Comparable { public int compareTo(SuperClass s) { ... } } class SubClass extends SuperClass {} // without overloading of compareTo() List superList = ...; sort(superList); List subList = ...; sort(subList); 

The compiler accepts the last line with

 static 
    
      void sort(List list) { ... } 
    

and rejects it with

static 
    
      void sort(List list) { ... } 
    

The reason for this rejection is that the type SubClass (which the compiler would determine from the type List in the parameter subList) is not suitable as a type parameter for T extends Comparable. The type SubClass doesn't implement Comparable; it only implements Comparable. The two elements are not compatible due to the lack of implicit covariance, although SubClass is compatible to SuperClass.

On the other hand, if we use , the compiler doesn't expect SubClass to implement Comparable; it's enough if SuperClass does it. It's enough because the method compareTo() is inherited from SuperClass and can be called for SubClass objects: expresses this, effecting contravariance.

Contravariant accessing variables of a type parameter

The upper or the lower bound applies only to type parameter of instantiations referred by a covariant or contravariant reference. In the case of Generic covariantReference; and Generic contravariantReference;, we can create and refer objects of different Generic instantiations.

Different rules are valid for the parameter and result type of a method (such as for input and output parameter types of a generic type). An arbitrary object compatible to SubType can be passed as parameter of the method write(), as defined above.

 contravariantReference.write(new SubType()); // OK contravariantReference.write(new SubSubType()); // OK too contravariantReference.write(new SuperType()); // type error ((Generic)contravariantReference).write( new SuperType()); // OK 

Because of contravariance, it's possible to pass a parameter to write(). This is in contrast to the covariant (also unbounded) wildcard type.

The situation doesn't change for the result type by binding: read() still delivers a result of type ?, compatible only to Object:

 Object o = contravariantReference.read(); SubType st = contravariantReference.read(); // type error 

The last line produces an error, even though we've declared a contravariantReference of type Generic.

The result type is compatible to another type only after the reference type has been explicitly converted:

 SuperSuperType sst = ((Generic)contravariantReference).read(); sst = (SuperSuperType)contravariantReference.read(); // unsafer alternative 

Examples in the previous listings show that reading or writing access to a variable of type parameter behaves the same way, regardless of whether it happens over a method (read and write) or directly (data in the examples).

Reading and writing to variables of type parameter

Table 1 shows that reading into an Object variable is always possible, because every class and the wildcard are compatible to Object. Writing an Object is possible only over a contravariant reference after appropriate casting, because Object is not compatible to the wildcard. Reading without casting into an unfitting variable is possible with a covariant reference. Writing is possible with a contravariant reference.

Table 1. Reading and writing access to variables of type parameter

reading

(input)

read

Object

write

Object

read

supertype   

write

supertype   

read

subtype    

write

subtype    

Wildcard

?

 OK  Error  Cast  Cast  Cast  Cast

Covariant

?extends

 OK  Error  OK  Cast  Cast  Cast

Contravariant

?super

 OK  Cast  Cast  Cast  Cast  OK

The rows in Table 1 refer to the sort of reference, and the columns to the type of data to be accessed. The headings of "supertype" and "subtype" indicate the wildcard bounds. The entry "cast" means the reference must be casted. An instance of "OK" in the last four columns refers to the typical cases for covariance and contravariance.

See the end of this article for a systematic test program for the table, with detailed explanations.

Creating objects

On the one hand, you cannot create objects of the wildcard type, because they are abstract. On the other hand, you can create array objects only of an unbounded wildcard type. You cannot create objects of other generic instantiations, however.

 Generic[] genericArray = new Generic[20]; // type error Generic[] wildcardArray = new Generic[20]; // OK genericArray = (Generic[])wildcardArray; // unchecked conversion genericArray[0] = new Generic(); genericArray[0] = new Generic(); // type error wildcardArray[0] = new Generic(); // OK 

Because of the covariance of arrays, the wildcard array type Generic[] is the supertype of the array type of all instantiations; therefore the assignment in the last line of the above code is possible.

Within a generic class, we cannot create objects of the type parameter. For example, in the constructor of an ArrayList implementation, the array object must be of type Object[] upon creation. We can then convert it to the array type of the type parameter:

 class MyArrayList implements List { private final E[] content; MyArrayList(int size) { content = new E[size]; // type error content = (E[])new Object[size]; // workaround } ... } 

For a safer workaround, pass the Class value of the actual type parameter to the constructor:

 content = (E[])java.lang.reflect.Array.newInstance(myClass, size); 

Multiple type parameters

A generic type can have more than one type parameter. Type parameters don't change the behavior of covariance and contravariance, and multiple type parameters can occur together, as shown below:

 class G {} G reference; reference = new G(); // without variance reference = new G(); // with co- and contravariance 

The generic interface java.util.Map is frequently used as an example for multiple type parameters. The interface has two type parameters, one for key and one for value. It's useful to associate objects with keys, for example so that we can more easily find them. A telephone book is an example of a Map object using multiple type parameters: the subscriber's name is the key, the phone number is the value.

The interface's implementation java.util.HashMap has a constructor for converting an arbitrary Map object into an association table:

 public HashMap(Map m) ... 

Because of covariance, the type parameter of the parameter object in this case does not have to correspond with the exact type parameter classes K and V. Instead, it can be adapted through covariance:

 Map customers; ... contacts = new HashMap(customers); // covariant 

Ici, Idest un supertype de CustomerNumber, et Personest un supertype de Customer.

Variance des méthodes

Nous avons parlé de la variance des types; passons maintenant à un sujet un peu plus facile.