Une nouvelle JEP propose de faciliter la manipulation du concept ésotérique de variance de type en Java. La nouvelle proposition, qui cible potentiellement Java 10, introduirait un moyen de spécifier la variance par défaut des types ciblés dans la définition de type générique, par opposition à la manière actuelle de l'indiquer par des caractères génériques lorsque le type générique est instancié. Cette proposition ne remplace pas les caractères génériques, mais est plutôt un moyen de réduire le besoin d'utiliser ces caractères.
Le sujet de la variance de type est encore assez obscur pour beaucoup de développeurs, et le fait que dans Java, ce problème ait été adressé à travers quelques caractères assez impopulaires n'a pas aidé à le rendre plus compréhensible. Pour cette raison, et pour nous assurer que nos lecteurs comprennent l'impact potentiel de cette JEP, nous allons d'abord expliquer ce qu'est la variance de type et comment elle est actuellement adressée en Java, suivie d'une description de ce que la nouvelle proposition permettrait.
Variance, Covariance et Contravariance
Considérez le code suivant appartenant à une application d'achat en ligne typique :
public class Product {
/* ... */
}
public class FrozenProduct extends Product {
/* ... */
}
Si nous avons une méthode scan(Product product)
et que nous l'appelons en passant un objet FrozenProduct
, l'appel se produit sans problèmes ; ceci est bien connu parmi les règles répandues du polymorphisme. Toutefois, lorsque les arguments incluent des génériques, la même logique ne peut pas être appliquée et le code suivant ne parvient pas à compiler :
private void addAllProducts(List<Product> products) {
/* Check product stock */
shoppingCart.addAll(products);
}
private void freezerSection() {
List<FrozenProduct> frozenProducts = /* select frozen products */;
addAllProducts(frozenProducts); // ERROR: List<Product> expected
// List<FrozenProduct> found
}
Lorsque des génériques sont utilisés en Java, aucune hypothèse n'est faite quant à la compatibilité du type cible par rapport à ses sous-types ou supertypes. En d'autres termes, lorsque des génériques sont utilisés, nous disons que le type cible est par défaut invariant : seul le type exact est accepté.
Toutefois, dans notre exemple ci-dessus, nous pouvons voir que la méthode addAllProducts
fonctionnerait avec une List
de Product
ou de ses sous-types. Lorsqu'un argument générique peut accepter soit son type cible, soit l'un de ses sous-types, on dit que le type est covariant, ce qui en Java est exprimé en utilisant extends
:
private void addAllProducts(List<? extends Product> products) {
/* Check product stock */
shoppingCart.addAll(products);
}
private void freezerSection() {
List<FrozenProduct> frozenProducts = /* select frozen products */;
addAllProducts(frozenProducts); // works with no problem
}
Dans ces exemples, la variance du type de cible qui est accepté est le sous-type. Il existe d'autres scénarios où la variance du type cible ne concerne pas les sous-types, mais les supertypes. Considérons le cas suivant :
private boolean askQuestion(Predicate<String> p) {
return p.test("hello");
}
private void applyPredicate() {
Predicate<Object> evenLength = o -> o.toString().length() % 2 == 0;
askQuestion(evenLength); // ERROR: Predicate<String> expected
// Predicate<Object> found
}
Dans ce cas, nous pouvons voir que l'application de la chaîne "hello"
à la lambda o -> o.toString().length() % 2 == 0
fonctionnerait sans problème ; toutefois, le compilateur ne nous laisse pas le faire. askQuestion
devrait être ok avec un Predicate
de String
ou n'importe quel de ses supertypes : dans ce cas, on dit que le type cible est contravariant, ce qui est exprimé en Java avec super
:
private boolean askQuestion(Predicate<? super String> p) {
return p.test("hello");
}
private void applyPredicate() {
Predicate<Object> evenLength = o -> o.toString().length() % 2 == 0;
askQuestion(evenLength); // works with no problem
}
Les caractères génériques sont un moyen très souple d'établir la variance de type car cela permet de définir différentes variances pour les mêmes types à des endroits divers. Par exemple, dans l'exemple ci-dessus, nous avons décidé que addAllProducts
devrait avoir un argument covariant, mais nous pouvons le rendre contravariant ou invariant dans d'autres endroits si cela convient à nos besoins. L'inconvénient, cependant, est que l'on doit explicitement spécifier la variance à chaque endroit nécessaire, ce qui ajoute rapidement beaucoup de verbosité et de confusion. C'est ici que la nouvelle proposition intervient.
Indiquer la variance par défaut au niveau de la déclaration
Un des principaux problèmes avec les caractères génériques, c'est qu'ils sont en général plus flexibles que nécessaire. Dans l'exemple de Predicate<String>
, on pourrait en théorie créer une méthode qui attend un Predicate<? extends String>
, cependant, le nombre d'usages où cela serait utile est très limité (s'il en existe). Pour un grand nombre de cas, il n'y a qu'une seule variance de type qui ait du sens, et pour refléter cela, la JEP 300 propose un moyen d'indiquer la variance par défaut au moment de la déclaration du type générique, et non au moment de l'instanciation. Par exemple, avec cette proposition, l'interface Predicate<T>
pourrait être réécrite avec le mot-clé proposé contravariant
comme Predicate<contravariant T>
, ce qui signifie que chaque fois qu'un développeur écrirait Predicate<String>
, ce serait implicitement interprété comme Predicate<? super String>
.
La syntaxe de cette nouvelle fonctionnalité n'a pas été décidée, bien que quelques options aient été suggérées : en utilisant de nouveaux mots-clés explicites comme Function<contravariant T, covariant R>
ou suivre l'exemple d'autres langages, comme les symboles dans Scala (Function<-T, +R>
) ou des mots-clés plus courts en C# (Function<in T, out R>
). Avant d'aborder la question de la syntaxe, certains aspects techniques importants doivent être abordés, à savoir l'interaction entre la variance par défaut et les caractères génériques, l'impact de la variance par défaut sur le code existant et le mécanisme concret par lequel la compatibilité des types de variance sera vérifiée.
Enfin, il convient de souligner que la JEP 300 ne traitera que de la création de la nouvelle capacité de variance par défaut mais ne modifiera aucune des classes et interfaces disponibles dans la bibliothèque Java ; ce type de travail devrait se produire si la JEP 300 devait aller de l'avant mais serait exécutée sous un JEP différent.