Ceci est un exemple concret tiré d'une API de bibliothèque tierce, mais simplifiée.
compilé avec Oracle JDK 8u72
Considérez ces deux méthodes:
<X extends CharSequence> X getCharSequence() {
return (X) "hello";
}
<X extends String> X getString() {
return (X) "hello";
}
Les deux rapportent un avertissement "non contrôlé" - je comprends pourquoi. La chose qui me déconcerte est pourquoi puis-je appeler
Integer x = getCharSequence();
et ça compile? Le compilateur doit savoir que Integer
n'implémente pas CharSequence
. L'appel à
Integer y = getString();
donne une erreur (comme prévu)
incompatible types: inference variable X has incompatible upper bounds Java.lang.Integer,Java.lang.String
Quelqu'un peut-il expliquer pourquoi ce comportement serait considéré comme valide? Comment cela serait-il utile?
Le client ne sait pas que cet appel est dangereux: le code du client est compilé sans avertissement. Pourquoi la compilation ne met-elle pas en garde à ce sujet/ne génère-t-il pas une erreur?
Aussi, en quoi est-il différent de cet exemple:
<X extends CharSequence> void doCharSequence(List<X> l) {
}
List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles
List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error
Essayer de passer List<Integer>
donne une erreur, comme prévu:
method doCharSequence in class generic.GenericTest cannot be applied to given types; required: Java.util.List<X> found: Java.util.List<Java.lang.Integer> reason: inference variable X has incompatible bounds equality constraints: Java.lang.Integer upper bounds: Java.lang.CharSequence
Si cela est signalé comme une erreur, pourquoi Integer x = getCharSequence();
ne l’est pas?
CharSequence
est un interface
. Par conséquent, même si SomeClass
n’implémente pas CharSequence
, il serait parfaitement possible de créer une classe.
class SubClass extends SomeClass implements CharSequence
Donc tu peux écrire
SomeClass c = getCharSequence();
parce que le type inféré X
est le type d'intersection SomeClass & CharSequence
.
Ceci est un peu étrange dans le cas de Integer
parce que Integer
est final, mais final
ne joue aucun rôle dans ces règles. Par exemple, vous pouvez écrire
<T extends Integer & CharSequence>
D'autre part, String
n'est pas un interface
, il serait donc impossible d'étendre SomeClass
pour obtenir un sous-type de String
, car Java ne prend pas en charge l'héritage multiple pour les classes.
Avec l'exemple List
, vous devez vous rappeler que les génériques ne sont ni covariants ni contravariants. Cela signifie que si X
est un sous-type de Y
, List<X>
n'est ni un sous-type ni un sur-type de List<Y>
. Puisque Integer
n'implémente pas CharSequence
, vous ne pouvez pas utiliser List<Integer>
dans votre méthode doCharSequence
.
Vous pouvez cependant le faire compiler
<T extends Integer & CharSequence> void foo(List<T> list) {
doCharSequence(list);
}
Si vous avez une méthode qui renvoie un List<T>
comme ceci:
static <T extends CharSequence> List<T> foo()
tu peux faire
List<? extends Integer> list = foo();
Encore une fois, cela est dû au fait que le type inféré est Integer & CharSequence
et qu’il s’agit d’un sous-type de Integer
.
Les types d'intersection sont implicites lorsque vous spécifiez plusieurs bornes (par exemple, <T extends SomeClass & CharSequence>
).
Pour plus d'informations, ici est la partie du JLS où il explique le fonctionnement des limites de type. Vous pouvez inclure plusieurs interfaces, par exemple.
<T extends String & CharSequence & List & Comparator>
mais seule la première borne peut être une non-interface.
Le type induit par votre compilateur avant l'affectation de X
est Integer & CharSequence
. Ce type semble bizarre, car Integer
est final, mais c'est un type parfaitement valide en Java. Il est ensuite converti en Integer
, ce qui est parfaitement correct.
Il existe exactement une valeur possible pour le type Integer & CharSequence
: null
. Avec l'implémentation suivante:
<X extends CharSequence> X getCharSequence() {
return null;
}
L'affectation suivante fonctionnera:
Integer x = getCharSequence();
En raison de cette valeur possible, il n'y a aucune raison pour que l'attribution soit erronée, même si elle est évidemment inutile. Un avertissement serait utile.
En fait, j'ai récemment blogué à propos de ceci anti-pattern de conception d'API . Vous ne devriez (presque) jamais concevoir de méthode générique pour renvoyer des types arbitraires, car vous ne pouvez (presque) jamais garantir que le type inféré sera livré. Les méthodes telles que Collections.emptyList()
constituent une exception, dans le cas où le vide de la liste (et l'effacement du type générique) est la raison pour laquelle toute inférence pour <T>
fonctionnera:
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}