Qual é a diferença entre <? estende a Base> e <T estende a Base>?

29

Neste exemplo:

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() falha ao compilar com:

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>>());
                      ^

enquanto compiles() é aceito pelo compilador.

Esta resposta explica que a única diferença é que <? ...>, ao contrário ,<T ...> permite fazer referência ao tipo mais tarde, o que não parece ser o caso.

Qual é a diferença entre <? extends Number>e <T extends Number>neste caso e por que o primeiro não é compilado?

Dev Null
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew
[1] Tipo genérico Java: diferença entre a lista <? extende Number> e List <T extends Number> parece estar fazendo a mesma pergunta, mas embora possa ser interessante, não é realmente uma duplicata. [2] Embora essa seja uma boa pergunta, o título não reflete adequadamente a pergunta específica que está sendo feita na sentença final.
skomisa 7/03
Isso já foi respondido aqui Java Generic List <List <? extends Number >>
Mạnh Quyết Nguyễn
E a explicação aqui ?
jrook 8/03

Respostas:

14

Definindo o método com a seguinte assinatura:

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

e invocando-o como:

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

Nos jls §8.1.2 , encontramos (parte interessante em negrito por mim):

Uma declaração de classe genérica define um conjunto de tipos parametrizados (§4.5), um para cada chamada possível da seção de parâmetro de tipo por argumentos de tipo . Todos esses tipos parametrizados compartilham a mesma classe em tempo de execução.

Em outras palavras, o tipo Té comparado com o tipo de entrada e atribuído Integer. A assinatura se tornará efetivamente static void compiles(Map<Integer, List<Integer>> map).

Quando se trata de doesntCompilemétodo, jls define regras de subtipagem ( §4.5.1 , negrito por mim):

Diz-se que um argumento de tipo T1 contém outro argumento de tipo T2, escrito T2 <= T1, se o conjunto de tipos indicado por T2 for provavelmente um subconjunto do conjunto de tipos indicado por T1 sob o fechamento reflexivo e transitivo das regras a seguir ( onde <: denota subtipagem (§4.10)):

  • ? estende T <=? estende S se T <: S

  • ? estende T <=?

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

  • ? super T <=?

  • ? super T <=? estende o objeto

  • T <= T

  • T <=? estende T

  • T <=? super T

Isso significa que, de ? extends Numberfato, contém Integerou até List<? extends Number>contém List<Integer>, mas não é o caso de Map<Integer, List<? extends Number>>e Map<Integer, List<Integer>>. Mais sobre esse tópico pode ser encontrado neste tópico do SO . Você ainda pode fazer a versão com ?curinga declarar que espera um subtipo 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
fonte
[1] Eu acho que você quis dizer ? extends Numbermais do que ? extends Numeric. [2] Sua afirmação de que "não é o caso da Lista <? Estende o Número> e da Lista <Integer>" está incorreta. Como o @VinceEmigh já apontou, você pode criar um método static void demo(List<? extends Number> lst) { }e chamá-lo assim demo(new ArrayList<Integer>());ou assim demo(new ArrayList<Float>());, e o código compila e executa OK. Ou talvez eu esteja interpretando mal ou entendendo mal o que você declarou?
skomisa 9/03
@ Você está certo nos dois casos. Quando se trata do seu segundo ponto, escrevi de maneira enganosa. Eu quis dizer List<? extends Number>como um parâmetro de tipo de todo o mapa, não ele próprio. Muito obrigado pelo comentário.
Andronicus
@skomisa pelo mesmo motivo List<Number>não contém List<Integer>. Suponha que você tenha uma função static void check(List<Number> numbers) {}. Ao invocar com check(new ArrayList<Integer>());ele não compila, você deve definir o método como static void check(List<? extends Number> numbers) {}. Com o mapa é o mesmo, mas com mais aninhamento.
Andronicus
11
@skomisa, assim como Numberé um parâmetro de tipo da lista e você precisa adicionar ? extendspara torná-lo covariante, List<? extends Number>é um parâmetro de tipo Mape também precisa ? extendsde covariância.
Andronicus
11
ESTÁ BEM. Como você forneceu a solução para um curinga multinível (também conhecido como "curinga aninhada" ?) E vinculado à referência JLS relevante, tenha a recompensa.
skomisa 11/03
6

Na chamada:

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

T é correspondido a Inteiro, portanto, o tipo do argumento é a Map<Integer,List<Integer>>. Não é o caso do método doesntCompile: o tipo do argumento permanece Map<Integer, List<? extends Number>>independentemente do argumento real na chamada; e isso não é atribuível a partir de HashMap<Integer, List<Integer>>.

ATUALIZAR

No doesntCompilemétodo, nada impede que você faça algo assim:

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

Então, obviamente, ele não pode aceitar a HashMap<Integer, List<Integer>>como argumento.

Maurice Perry
fonte
Como seria uma chamada válida doesntCompile? Apenas curioso sobre isso.
Xtreme Biker
11
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());funcionaria, como faria doesntCompile(new HashMap<>());.
skomisa 5/03
@XtremeBiker, mesmo isso funcionaria, Mapa <Inteiro, Lista <? estende Número >> map = new HashMap <Inteiro, Lista <? estende o número >> (); map.put (nulo, novo ArrayList <Integer> ()); doesntCompile (mapa);
MOnkey 5/03
"que não pode ser atribuído a partir de HashMap<Integer, List<Integer>>" você poderia explicar por que não pode ser atribuído a partir dele?
Dev Null
@DevNull veja minha atualização acima
Maurice Perry
2

Exemplo simplificado de demonstração. O mesmo exemplo pode ser visualizado como abaixo.

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>>é um tipo de curinga de vários níveis, enquanto List<? extends Number>é um tipo de curinga padrão.

Instanciações concretas válidas do tipo curinga List<? extends Number>incluem Numbere quaisquer subtipos de Numberconsiderando que, no caso deList<Pair<? extends Number>> que seja um argumento de tipo de argumento de tipo e ele próprio tenha uma instanciação concreta do tipo genérico.

Os genéricos são invariantes, portanto, Pair<? extends Number>o tipo de curinga só pode aceitar Pair<? extends Number>>. O tipo interno ? extends Numberjá é covariante. Você precisa tornar o tipo de anexo como covariável para permitir covariância.

Sagar Veeram
fonte
Como é que <Pair<Integer>>isso não funciona, <Pair<? extends Number>>mas funciona <T extends Number> <Pair<T>>?
jaco0646 12/03
@ jaco0646 Você está basicamente fazendo a mesma pergunta que o OP, e a resposta da Andronicus foi aceita. Veja o exemplo de código nessa resposta.
skomisa 15/03
@skomisa, sim, estou fazendo a mesma pergunta por algumas razões: uma é que essa resposta não parece realmente abordar a pergunta do OP; mas a segunda é que acho essa resposta mais fácil de entender. Eu não posso seguir a resposta de Andronicus de qualquer maneira que me leva a entender aninhados vs genéricos não-aninhadas, ou mesmo Tvs ?. Parte do problema é que, quando Andronicus alcança o ponto vital de sua explicação, ele adia outro tópico que usa apenas exemplos triviais. Eu esperava obter uma resposta mais clara e completa aqui.
jaco0646 16/03
11
@ jaco0646 OK. O documento "Perguntas frequentes sobre genéricos de Java - argumentos de tipo", de Angelika Langer, possui uma FAQ intitulada O que significam curingas de vários níveis (ou seja, aninhados)? . Essa é a melhor fonte que conheço para explicar as questões levantadas na pergunta do OP. As regras para curingas aninhados não são diretas nem intuitivas.
skomisa 16/03
1

Eu recomendo que você procure na documentação de curingas genéricos, especialmente diretrizes para o uso de curingas

Falando francamente do seu método #doesntCompile

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

e ligue como

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

É fundamentalmente incorreto

Vamos adicionar a implementação legal :

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

É muito bom, porque Double estende Number, então put também List<Double>é absolutamente bom List<Integer>, certo?

No entanto, você ainda acha que é legal passar aqui new HashMap<Integer, List<Integer>>()do seu exemplo?

O compilador não pensa assim e está fazendo o possível para evitar tais situações.

Tente fazer a mesma implementação com o método #compile e o compilador obviamente não permitirá que você coloque uma lista de duplas no mapa.

    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
    }

Basicamente, você não pode colocar nada, mas List<T>é por isso que é seguro chamar esse método com new HashMap<Integer, List<Integer>>()ou new HashMap<Integer, List<Double>>()ou new HashMap<Integer, List<Long>>()ou new HashMap<Integer, List<Number>>().

Então, em poucas palavras, você está tentando trapacear com o compilador e ele se defende bastante contra esse tipo de trapaça.

Nota: a resposta enviada por Maurice Perry está absolutamente correta. Só não tenho certeza se está claro o suficiente, então tentei (realmente espero que eu conseguisse) adicionar uma postagem mais extensa.

Makhno
fonte