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

171

Il s'agit d'un exemple concret d'une API de bibliothèque tierce, mais simplifié.

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 "casting non vérifié" - je comprends pourquoi. La chose qui me déroute est pourquoi puis-je appeler

Integer x = getCharSequence();

et il compile? Le compilateur doit savoir que cela Integern'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-il considéré comme valide? En quoi cela serait-il utile?

Le client ne sait pas que cet appel est dangereux - le code du client se compile sans avertissement. Pourquoi la compilation ne l'avertirait-elle pas / n'émettrait-elle pas une erreur?

En outre, 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 réussir 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();pas?

Adam Michalik
la source
15
intéressant! le casting sur le LHS Integer x = getCharSequence();sera compilé, mais le casting sur le RHS Integer x = (Integer) getCharSequence();échoue à la compilation
flocons
Quelle version du compilateur java utilisez-vous? Veuillez préciser cette information dans la question.
Federico Peralta Schaffner
@FedericoPeraltaSchaffner ne peut pas voir pourquoi c'est important - c'est une question directement sur le JLS.
Boris the Spider
@BoristheSpider Parce que le mécanisme d'inférence de type a changé pour java8
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner - J'ai déjà tagué la question avec [java-8], mais j'ai ajouté la version du compilateur dans le message maintenant.
Adam Michalik le

Réponses:

184

CharSequenceest un interface. Par conséquent, même si SomeClassne met pas en œuvre CharSequenceil serait parfaitement possible de créer une classe

class SubClass extends SomeClass implements CharSequence

Par conséquent, vous pouvez écrire

SomeClass c = getCharSequence();

car le type inféré Xest le type d'intersection SomeClass & CharSequence.

C'est un peu étrange dans le cas de Integerparce que Integerc'est définitif, mais finalne joue aucun rôle dans ces règles. Par exemple, vous pouvez écrire

<T extends Integer & CharSequence>

D'un autre côté, ce Stringn'est pas un interface, il serait donc impossible d'étendre SomeClasspour obtenir un sous-type de String, car java ne prend pas en charge l'héritage multiple pour les classes.

Avec l' Listexemple, vous devez vous rappeler que les génériques ne sont ni covariants ni contravariants. Cela signifie que si Xest un sous-type de Y, List<X>n'est ni un sous-type ni un supertype de List<Y>. Puisque Integerne met pas en œuvre CharSequence, vous ne pouvez pas utiliser List<Integer>dans votre doCharSequenceméthode.

Vous pouvez cependant obtenir ceci pour compiler

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Si vous avez une méthode qui renvoie un List<T>comme celui-ci:

static <T extends CharSequence> List<T> foo() 

tu peux faire

List<? extends Integer> list = foo();

Encore une fois, c'est parce que le type inféré est Integer & CharSequenceet qu'il s'agit d'un sous-type de Integer.

Les types d'intersection se produisent implicitement lorsque vous spécifiez plusieurs limites (par exemple <T extends SomeClass & CharSequence>).

Pour plus d'informations, voici 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.

Paul Boddington
la source
62
Je ne savais pas que vous pouviez mettre un &dans la définition générique. +1
flocons
13
@flkes Vous pouvez en mettre plusieurs, mais seul le premier argument peut être une non-interface. <T extends String & List & Comparator>est ok mais <T extends String & Integer>ne l'est pas, car ce Integern'est pas une interface.
Paul Boddington
7
@PaulBoddington Il existe une certaine utilisation pratique de ces méthodes. Par exemple, si le type n'est pas réellement utilisé pour les données stockées. Des exemples pour cela sont Collections.emptyList()ainsi que Optional.empty(). Ceux-ci renvoient des implémentations d'une interface générique, mais ne stockent rien.
Stefan Dollase le
6
Et personne ne dit qu'une classe en cours finalde compilation le sera finalau moment de l'exécution.
Holger
7
@Federico Peralta Schaffner: le point ici est que la méthode getCharSequence()promet de retourner tout ce dont Xl'appelant a besoin, ce qui inclut le retour d'un type étendant Integeret implémentant CharSequencesi l'appelant en a besoin et sous cette promesse, il est correct d'autoriser l'attribution du résultat à Integer. C'est la méthode getCharSequence()qui est cassée car elle ne tient pas sa promesse, mais ce n'est pas la faute du compilateur.
Holger le
59

Le type qui est déduit par votre compilateur avant l'affectation pour Xest Integer & CharSequence. Ce type semble étrange, car il Integerest définitif, mais c'est un type parfaitement valide en Java. Il est ensuite jeté Integer, ce qui est parfaitement OK.

Il y a exactement une valeur possible pour le Integer & CharSequencegenre: null. Avec l'implémentation suivante:

<X extends CharSequence> X getCharSequence() {
    return null;
}

La mission suivante fonctionnera:

Integer x = getCharSequence();

En raison de cette valeur possible, il n'y a aucune raison pour que l'affectation 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é sur cet anti-pattern de conception d'API . Vous ne devriez (presque) jamais concevoir une méthode générique pour renvoyer des types arbitraires car vous ne pouvez (presque) jamais garantir que le type inféré sera livré. Une exception sont des méthodes comme Collections.emptyList(), dans le cas desquelles le vide de la liste (et l'effacement de type générique) est la raison pour laquelle toute inférence <T>fonctionnera:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
la source