Quelle est la difference entre <? étend Base> et <T étend Base>?

29

Dans cet exemple:

import java.util.*;

public class Example {
    static void doesntCompile(Map<Integer, List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    static void function(List<? extends Number> outer)
    {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

doesntCompile() ne parvient pas à compiler avec:

Example.java:9: error: incompatible types: HashMap<Integer,List<Integer>> cannot be converted to Map<Integer,List<? extends Number>>
        doesntCompile(new HashMap<Integer, List<Integer>>());
                      ^

while compiles()est accepté par le compilateur.

Cette réponse explique que la seule différence est que, contrairement à <? ...>, <T ...>vous permet de référencer le type plus tard, ce qui ne semble pas être le cas.

Quelle est la différence entre <? extends Number>et <T extends Number>dans ce cas et pourquoi la première compilation ne se fait-elle pas?

Dev Null
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew
[1] Type générique Java: différence entre List <? étend Number> et List <T étend Number> semble poser la même question, mais bien que cela puisse être intéressant, ce n'est pas vraiment un doublon. [2] Bien qu'il s'agisse d'une bonne question, le titre ne reflète pas correctement la question spécifique posée dans la dernière phrase.
skomisa
C'est déjà répondu ici Java Generic List <Liste <? étend le nombre >>
Mạnh Quyết Nguyễn
Et l'explication ici ?
jrook

Réponses:

14

En définissant la méthode avec la signature suivante:

static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

et l'invoquer comme:

compiles(new HashMap<Integer, List<Integer>>());

Dans le jls §8.1.2, nous trouvons que (partie intéressante en gras pour moi):

Une déclaration de classe générique définit un ensemble de types paramétrés (§4.5), un pour chaque appel possible de la section de paramètres de type par arguments de type . Tous ces types paramétrés partagent la même classe au moment de l'exécution.

En d'autres termes, le type Test comparé au type d'entrée et affecté Integer. La signature deviendra effectivement static void compiles(Map<Integer, List<Integer>> map).

En matière de doesntCompileméthode, jls définit des règles de sous-typage ( §4.5.1 , mis en gras par moi):

Un argument de type T1 est censé contenir un autre argument de type T2, écrit T2 <= T1, si l'ensemble des types dénotés par T2 est de manière prouvable un sous-ensemble de l'ensemble des types dénotés par T1 sous la fermeture réflexive et transitive des règles suivantes ( où <: désigne le sous-typage (§4.10)):

  • ? étend T <=? étend S si T <: S

  • ? étend T <=?

  • ? super T <=? super S si S <: T

  • ? super T <=?

  • ? super T <=? étend l'objet

  • T <= T

  • T <=? étend T

  • T <=? super T

Cela signifie, en ? extends Numbereffet, contient Integerou List<? extends Number>contient List<Integer>, mais ce n'est pas le cas pour Map<Integer, List<? extends Number>>et Map<Integer, List<Integer>>. Plus sur ce sujet peut être trouvé dans ce fil SO . Vous pouvez toujours faire fonctionner la version avec ?caractère générique en déclarant que vous attendez un sous-type de List<? extends Number>:

public class Example {
    // now it compiles
    static void doesntCompile(Map<Integer, ? extends List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    public static void main(String[] args) {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}
Andronicus
la source
[1] Je pense que vous vouliez dire ? extends Numberplutôt que ? extends Numeric. [2] Votre affirmation selon laquelle "ce n'est pas le cas pour List <? Extend Number> et List <Integer>" est incorrecte. Comme @VinceEmigh l'a déjà souligné, vous pouvez créer une méthode static void demo(List<? extends Number> lst) { }et l'appeler comme ceci demo(new ArrayList<Integer>());ou cela demo(new ArrayList<Float>());, et le code se compile et s'exécute OK. Ou suis-je peut-être mal interprété ou mal compris ce que vous avez déclaré?
skomisa
@ Vous avez raison dans les deux cas. En ce qui concerne votre deuxième point, je l'ai écrit de manière trompeuse. Je voulais dire List<? extends Number>comme paramètre de type de la carte entière, pas elle-même. Merci beaucoup pour le commentaire.
Andronicus
@skomisa de la même raison List<Number>ne contient pas List<Integer>. Supposons que vous ayez une fonction static void check(List<Number> numbers) {}. Lorsque l'invocation avec check(new ArrayList<Integer>());elle ne se compile pas, vous devez définir la méthode comme static void check(List<? extends Number> numbers) {}. Avec la carte, c'est la même chose mais avec plus d'imbrication.
Andronicus
1
@skomisa, tout comme Numberun paramètre de type de liste et que vous devez ajouter ? extendspour le rendre covariant, List<? extends Number>est un paramètre de type de Mapet a également besoin ? extendsde covariance.
Andronicus
1
D'ACCORD. Étant donné que vous avez fourni la solution pour un caractère générique à plusieurs niveaux (alias "caractère générique imbriqué" ?), Et lié à la référence JLS appropriée, ayez la prime.
skomisa
6

Dans l'appel:

compiles(new HashMap<Integer, List<Integer>>());

T correspond à Integer, donc le type de l'argument est a Map<Integer,List<Integer>>. Ce n'est pas le cas pour la méthode doesntCompile: le type de l'argument reste Map<Integer, List<? extends Number>>quel que soit l'argument réel dans l'appel; et ce n'est pas attribuable à partir de HashMap<Integer, List<Integer>>.

MISE À JOUR

Dans la doesntCompileméthode, rien ne vous empêche de faire quelque chose comme ceci:

static void doesntCompile(Map<Integer, List<? extends Number>> map) {
    map.put(1, new ArrayList<Double>());
}

Alors évidemment, il ne peut pas accepter un HashMap<Integer, List<Integer>>comme argument.

Maurice Perry
la source
À quoi ressemblerait alors un appel valide doesntCompile? Juste curieux à ce sujet.
Xtreme Biker
1
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());fonctionnerait, tout comme doesntCompile(new HashMap<>());.
skomisa
@XtremeBiker, même cela fonctionnerait, Map <Integer, List <? étend Number >> map = new HashMap <Entier, Liste <? étend Number >> (); map.put (null, new ArrayList <Integer> ()); doesntCompile (carte);
MOnkey
"qui n'est pas attribuable à HashMap<Integer, List<Integer>>" pourriez-vous expliquer pourquoi il ne peut pas être attribué à partir de cela?
Dev Null
@DevNull voir ma mise à jour ci
Maurice Perry
2

Exemple simplifié de démonstration. Le même exemple peut être visualisé comme ci-dessous.

static void demo(List<Pair<? extends Number>> lst) {} // doesn't work
static void demo(List<? extends Pair<? extends Number>> lst) {} // works
demo(new ArrayList<Pair<Integer>()); // works
demo(new ArrayList<SubPair<Integer>()); // works for subtype too

public static class Pair<T> {}
public static class SubPair<T> extends Pair<T> {}

List<Pair<? extends Number>>est un type de caractères génériques à plusieurs niveaux alors que List<? extends Number>c'est un type de caractère générique standard.

Les instanciations concrètes valides du type générique List<? extends Number>incluent Numberet tout sous-type Numberalors que dans le cas List<Pair<? extends Number>>où il s'agit d'un argument de type argument de type et a lui-même une instanciation concrète de type générique.

Les génériques sont invariants donc Pair<? extends Number>le type de caractère générique ne peut accepter Pair<? extends Number>>. Le type interne ? extends Numberest déjà covariant. Vous devez rendre le type englobant comme covariant pour autoriser la covariance.

Sagar Veeram
la source
Comment est-ce qui <Pair<Integer>>ne fonctionne pas <Pair<? extends Number>>mais fonctionne avec <T extends Number> <Pair<T>>?
jaco0646
@ jaco0646 Vous posez essentiellement la même question que l'OP, et la réponse d'Andronicus a été acceptée. Voir l'exemple de code dans cette réponse.
skomisa
@skomisa, oui, je pose la même question pour deux raisons: la première est que cette réponse ne semble pas réellement répondre à la question du PO; mais deux est que je trouve cette réponse plus facile à comprendre. Je ne peux pas suivre la réponse de Andronicus d'une façon qui me conduit à comprendre imbriqués vs génériques non imbriqués, ou même Tcontre ?. Une partie du problème est que lorsque Andronicus atteint le point essentiel de son explication, il s'en remet à un autre fil qui n'utilise que des exemples triviaux. J'espérais obtenir une réponse plus claire et plus complète ici.
jaco0646
1
@ jaco0646 OK. Le document "Java Generics FAQs - Type Arguments" par Angelika Langer a une FAQ intitulée Que signifient les caractères génériques à plusieurs niveaux (c'est-à-dire imbriqués)? . C'est la meilleure source que je connaisse pour expliquer les questions soulevées dans la question du PO. Les règles pour les caractères génériques imbriqués ne sont ni simples ni intuitives.
skomisa
1

Je vous recommande de consulter la documentation des caractères génériques génériques, en particulier les directives d'utilisation des caractères génériques

Franchement, votre méthode #doesntCompile

static void doesntCompile(Map<Integer, List<? extends Number>> map) {}

et appeler comme

doesntCompile(new HashMap<Integer, List<Integer>>());

Est fondamentalement incorrect

Ajoutons l' implémentation juridique :

    static void doesntCompile(Map<Integer, List<? extends Number>> map) {
        List<Double> list = new ArrayList<>();
        list.add(0.);
        map.put(0, list);
    }

C'est très bien, parce que Double étend Number, donc List<Double>c'est tout aussi bien List<Integer>, non?

Cependant, pensez-vous toujours qu'il est légal de passer ici new HashMap<Integer, List<Integer>>()de votre exemple?

Le compilateur ne le pense pas et fait de son mieux pour éviter de telles situations.

Essayez de faire la même implémentation avec la méthode #compile et le compilateur ne vous permettra évidemment pas de mettre une liste de doubles dans la carte.

    static <T extends Number> void compiles(Map<Integer, List<T>> map) {
        List<Double> list = new ArrayList<>();
        list.add(10.);
        map.put(10, list); // does not compile
    }

Fondamentalement, vous ne pouvez rien mettre mais List<T>c'est pourquoi il est sûr d'appeler cette méthode avec new HashMap<Integer, List<Integer>>()ou new HashMap<Integer, List<Double>>()ou new HashMap<Integer, List<Long>>()ou new HashMap<Integer, List<Number>>().

Donc, en un mot, vous essayez de tricher avec le compilateur et il se défend assez contre une telle tricherie.

NB: la réponse postée par Maurice Perry est absolument correcte. Je ne suis pas sûr que ce soit assez clair, alors j'ai essayé (j'espère vraiment que j'ai réussi) d'ajouter un article plus complet.

Makhno
la source