juin
2014
Maintenant que Java 8 est sorti… on va en profiter pour regarder un peu plus loin et viser Java 9.
Il est encore trop tôt pour en avoir un aperçu détaillé, mais une « petite » proposition d’évolution a retenue mon attention.
Covariance et Contravariance (Java 5.0)
Actuellement, les types Generics possèdent trois niveaux de variance, permettant de faire varier le paramétrage Generics.
Prenons le cas du type List>Number<.
Le paramétrage Generics est invariant, ce qui signifie que l’on ne peut pas « modifier » le type paramétré, que ce soit pour l’affecter à une autre instance ou le passer en paramètre à un méthode :
List<Object> listOfObjects = listOfNumbers; // Erreur de compilation !
List<Integer> listOfIntegers = listOfNumbers; // Erreur de compilation !
Ce type d’erreur est tout à fait normal, sinon on pourrait se retrouver dans un état incohérent sans connaitre le type exact, ce qui provoquerait des erreurs à l’exécution (ce que les Generics permettent d’éviter).
En effet les bouts de code suivant sont tout à fait correct, mais provoquerait des erreurs si la vrai liste contient des objets d’un autre type :
Integer value = listOfIntegers.get(0); // ???
Toutefois il est souvent utile de travailler avec un type moins spécifique, et la notion de variance permet de pallier à cela, en permettant de définir un type de manière plus ou moins restrictive :
- La covariance permet de définir un type qui peut recevoir n’importe quel type plus spécifique en limitant son utilisation.
Puisqu’on ne connait pas le type exact du paramétrage (mais seulement un de ses parents), on ne peut pas utiliser les méthodes qui utilisent le type paramétrer en paramètre.
En effet sans connaitre le type exact le compilateur ne pourra pas vérifier la cohérence de l’ensemble :List<Double> listOfDoubles = new ArrayList();
List<? extends Number> listOfNumbers = listOfDoubles; // OK
// On ne connait pas le type réel retourné par get()
// Mais on sait que c'est un type qui étend Number
// donc le code suivant est tout à fait correct :
Number number = listOfNumbers.get(0); // OK
// On ne connait pas le type réel du paramètre de add()
// Cela pourrait être n'importe quel type qui étend Number
// donc le compilateur ne peut pas vérifier la cohérence :
listOfObjects.add(new Double(0.0)); // Erreur de compilation ! - A l’inverse la contravariance permet de définir un type qui peut recevoir n’importe quel type moins spécifique, mais on ne peut pas le récupérer en valeur de retour d’une méthode (sauf en tant qu’Object évidemment) :
List<Number> listOfNumbers = new ArrayList<Number>();
List<? super Double> listOfDoubles = listOfNumbers; // OK
// On ne connait pas le type réel retourné par get()
// On sait juste que c'est un type parent de Double
// (mais pas forcément un Double)
Double value = listOfDoubles.get(0); // Erreur de compilation !
// On ne connait pas le type réel du paramètre de add()
// Mais on sait que c'est un type parent de Double
// donc le code reste cohérent pour le compilateur :
listOfDoubles.add(new Double(0.0)); // OK
L’utilisation de la covariance et de la contravariance est primordiale car cela permet d’écrire du code plus générique, sans s’imposer des restrictions inutiles.
Prenons par exemple cette méthode, qui permet d’afficher tous les éléments d’une liste :
for (Object value : list) {
System.out.println(value);
}
}
Elle possède un gros défaut car elle ne peut être utilisée qu’avec un objet de type List<Object>. Impossible de l’utiliser avec une List<Integer>, List<String> ou encore une List<Date>, même si les spécificitées de ces listes nous sont inutile dans le cas présent !!!
La covariance permet donc de rendre notre méthode compatible avec toutes sortes de liste :
for (Object value : list) {
System.out.println(value);
}
}
La covariance/contravariance a donc une énorme importance dans l’utilisation des Generics… aus prix d’un énorme défaut : une syntaxe verbeuse et pas très lisible…
Quelles améliorations pour Java 9 ?
Brian Goetz a donc soumis une proposition pour améliorer cela dans certains cas.
En effet si de nombreux types Generics utilisent leurs types paramétrés en paramètres et en type de retour, il existe de nombreux types et interfaces plus simple, souvent composé d’une seule méthode, et qui l’utilisent généralement d’une seule et unique manière.
Dans ces cas là, l’utilisation « classique » du type Generics est identique à son équivalent covariant ou contravariant.
- Callable<V> définie principalement une méthode V call(), et son utilisation sera donc identique lorsqu’on l’utilise via la syntaxe covariante Callable<? extends V>.
- Predicate<T> définie principalement une méthode boolean test(T), et son utilisation sera donc identique lorsqu’on l’utilise via la syntaxe contravariante Predicate<? super T>.
- Function<T,R> définie principalement une méthode R apply(T), et son utilisation sera donc identique lorsqu’on l’utilise via la syntaxe covariante/contravariante Function<? extends T,? super R>.
- etc.
Tous ces types sont généralement utilisés sous leurs formes covariantes/contravariantes afin de conserver un code le plus générique possible. Et avec l’arrivée des lambdas et des interfaces fonctionnelles, les cas d’utilisation se multiplient… mais cela complexifient la lecture des types et des APIs.
La proposition consiste à permettre de définir la covariance/contravariance directement au niveau de la classe ou de l’interface, afin de ne pas avoir à le répéter tout le temps.
Par exemple actuellement l’interface Function<T,R> est définie de la manière suivante :
R apply(T t);
}
Il suffirait de définir la covariance dans sa déclaration, par exemple comme ceci :
R apply(T t);
}
Bien sûr cela ne serait uniquement possible que si le type variant est utilisé correctement au sein de l’interface.
Dans le cas présent le type covariant T ne doit être utilisé qu’en entrée (via un paramètre de méthode), et le type contravariant R ne doit être utilisé qu’en sortie (via une valeur de retour).
A partir de là le compilateur pourrait alors interpréter toutes les utilisations de Function<T,R> comme s’ils utilisaient la forme de variance Function<? extends T,? super R>.
Cela simplifierait la lisibilité de nombreuses méthodes de l’API, et la vie du développeur par la même occassion.
Source et détails complémentaires : Improved variance for generic classes and interfaces
Tutoriels
Discussions
- Recuperation du nom des parametres
- Possibilité d'accéder au type générique en runtime
- Difference de performances Unix/Windows d'un programme?
- Définition exacte de @Override
- Classes, méthodes private
- [ fuite ] memoire
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- jre 1.5, tomcat 6.0 et multi processeurs
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?