web-dev-qa-db-fra.com

Limite supérieure du type de retour générique - interface vs classe - code étonnamment valide

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?

170
Adam Michalik

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.

183
Paul Boddington

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.

Le vrai problème est l’API, pas le site d’appel

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;
}
59
Lukas Eder