¿Cuál es la diferencia entre <? extiende Base> y <T extiende Base>?

29

En este ejemplo:

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() falla al compilar con:

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

mientras compiles()es aceptado por el compilador.

Esta respuesta explica que la única diferencia es que <? ...>, a diferencia , le <T ...>permite hacer referencia al tipo más adelante, lo que no parece ser el caso.

¿Cuál es la diferencia entre <? extends Number>y <T extends Number>en este caso y por qué no se compila primero?

Dev Null
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew
[1] Tipo genérico Java: diferencia entre Lista <? extiende Número> y Lista <T extiende Número> parece estar haciendo la misma pregunta, pero si bien puede ser de interés, en realidad no es un duplicado. [2] Si bien esta es una buena pregunta, el título no refleja adecuadamente la pregunta específica que se hace en la oración final.
skomisa
Esto ya está respondido aquí Java Generic List <List <? extiende Número >>
Mạnh Quyết Nguyễn
¿Qué tal la explicación aquí ?
jrook

Respuestas:

14

Al definir el método con la siguiente firma:

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

e invocandolo como:

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

En el jls §8.1.2 encontramos que (parte interesante en negrita por mí):

Una declaración de clase genérica define un conjunto de tipos parametrizados (§4.5), uno para cada invocación posible de la sección de parámetros de tipo por argumentos de tipo . Todos estos tipos parametrizados comparten la misma clase en tiempo de ejecución.

En otras palabras, el tipo Tse compara con el tipo de entrada y se asigna Integer. La firma se convertirá efectivamente static void compiles(Map<Integer, List<Integer>> map).

Cuando se trata de doesntCompilemétodo, jls define reglas de subtipo ( §4.5.1 , en negrita):

Se dice que un argumento de tipo T1 contiene otro argumento de tipo T2, escrito T2 <= T1, si el conjunto de tipos denotado por T2 es probablemente un subconjunto del conjunto de tipos denotado por T1 bajo el cierre reflexivo y transitivo de las siguientes reglas ( donde <: denota subtipo (§4.10)):

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

  • ? extiende T <=?

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

  • ? super T <=?

  • ? super T <=? extiende objeto

  • T <= T

  • T <=? extiende T

  • T <=? super T

Esto significa que de ? extends Numberhecho contiene Integero incluso List<? extends Number>contiene List<Integer>, pero no es el caso de Map<Integer, List<? extends Number>>y Map<Integer, List<Integer>>. Se puede encontrar más sobre ese tema en este hilo SO . Aún puede hacer que la versión con ?comodín funcione declarando que espera un 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
fuente
[1] Creo que quisiste decir ? extends Numbermás que ? extends Numeric. [2] Su afirmación de que "no es el caso para la Lista <? Extiende el Número> y la Lista <Integer>" es incorrecta. Como @VinceEmigh ya ha señalado, puede crear un método static void demo(List<? extends Number> lst) { }y llamarlo así demo(new ArrayList<Integer>());o así demo(new ArrayList<Float>());, y el código se compila y se ejecuta OK. ¿O tal vez estoy interpretando mal o entendiendo mal lo que usted dijo?
skomisa
@ Tienes razón en ambos casos. Cuando se trata de su segundo punto, lo escribí de manera engañosa. Me refería a List<? extends Number>un parámetro tipo de todo el mapa, no a sí mismo. Muchas gracias por el comentario.
Andronicus
@skomisa de la misma razón List<Number>no contiene List<Integer>. Supongamos que tienes una función static void check(List<Number> numbers) {}. Cuando invocar con check(new ArrayList<Integer>());él no se compila, debe definir el método como static void check(List<? extends Number> numbers) {}. Con el mapa es igual pero con más anidamiento.
Andronicus
1
@skomisa al igual que Numberes un parámetro de tipo de lista y necesita agregarlo ? extendspara que sea covariante, List<? extends Number>es un parámetro de tipo Mapy también necesita ? extendscovarianza.
Andronicus
1
OKAY. Dado que proporcionó la solución para un comodín multinivel (también conocido como "comodín anidado" ?) Y vinculado a la referencia JLS relevante, tenga la recompensa.
skomisa
6

En la llamada:

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

T coincide con Integer, por lo que el tipo de argumento es a Map<Integer,List<Integer>>. No es el caso para el método doesntCompile: el tipo de argumento permanece Map<Integer, List<? extends Number>>sea ​​cual sea el argumento real en la llamada; y eso no es asignable desde HashMap<Integer, List<Integer>>.

ACTUALIZAR

En el doesntCompilemétodo, nada te impide hacer algo como esto:

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

Obviamente, no puede aceptar a HashMap<Integer, List<Integer>>como argumento.

Maurice Perry
fuente
doesntCompileEntonces, ¿cómo sería una llamada válida ? Solo curiosidad por eso.
Xtreme Biker
1
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());funcionaría, como lo haría doesntCompile(new HashMap<>());.
skomisa
@XtremeBiker, incluso esto funcionaría, Map <Integer, List <? extiende Número >> mapa = nuevo HashMap <Entero, Lista <? extiende el Número >> (); map.put (nulo, nueva ArrayList <Integer> ()); doesntCompile (mapa);
MOnkey
"que no se puede asignar HashMap<Integer, List<Integer>>", ¿podría explicar por qué no se puede asignar?
Dev Null
@DevNull ver mi actualización anterior
Maurice Perry
2

Ejemplo simplificado de demostración. El mismo ejemplo se puede visualizar como a continuación.

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>>es un tipo de comodines de varios niveles, mientras que List<? extends Number>es un tipo de comodín estándar.

Las instancias concretas válidas del tipo comodín List<? extends Number>incluyen Numbery cualquier subtipo de Numbermientras que en caso de List<Pair<? extends Number>>que sea un argumento tipo de argumento tipo y en sí mismo tenga una instanciación concreta del tipo genérico.

Los genéricos son invariables, por lo que Pair<? extends Number>el tipo de comodín solo puede aceptar Pair<? extends Number>>. El tipo interno ? extends Numberya es covariante. Debe hacer que el tipo de cierre sea covariante para permitir la covarianza.

Sagar Veeram
fuente
¿Cómo es que eso <Pair<Integer>>no funciona <Pair<? extends Number>>pero sí funciona <T extends Number> <Pair<T>>?
jaco0646
@ jaco0646 Usted esencialmente hace la misma pregunta que el OP, y la respuesta de Andronicus ha sido aceptada. Vea el ejemplo de código en esa respuesta.
skomisa
@skomisa, sí, estoy haciendo la misma pregunta por un par de razones: una es que esta respuesta en realidad no parece responder a la pregunta del OP; pero dos es que esta respuesta me resulta más fácil de comprender. No puedo seguir la respuesta de Andrónico de ninguna manera que los cables mí entender anidados vs genéricos no anidados, o incluso Tvs ?. Parte del problema es que cuando Andrónico llega al punto vital de su explicación, se desvía a otro hilo que usa solo ejemplos triviales. Esperaba obtener una respuesta más clara y completa aquí.
jaco0646
1
@ jaco0646 OK. El documento "Preguntas frecuentes sobre genéricos de Java - Argumentos de tipo" de Angelika Langer tiene una pregunta frecuente titulada ¿Qué significan los comodines de varios niveles (es decir, anidados)? . Esa es la mejor fuente que conozco para explicar los problemas planteados en la pregunta del OP. Las reglas para los comodines anidados no son sencillas ni intuitivas.
skomisa
1

Le recomiendo que consulte la documentación de comodines genéricos, especialmente las pautas para el uso de comodines

Hablando francamente tu método #doesntCompile

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

y llama como

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

Es fundamentalmente incorrecto

Agreguemos implementación legal :

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

Está realmente bien, porque Double extiende Number, por lo que put también List<Double>está absolutamente bien List<Integer>, ¿verdad?

Sin embargo, ¿todavía supones que es legal pasar aquí new HashMap<Integer, List<Integer>>()de tu ejemplo?

El compilador no lo cree, y está haciendo todo lo posible para evitar tales situaciones.

Intente hacer la misma implementación con el método #compile y el compilador obviamente no le permitirá poner una lista de dobles en el 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
    }

Básicamente, no puede poner nada, pero List<T>es por eso que es seguro llamar a ese método con new HashMap<Integer, List<Integer>>()o new HashMap<Integer, List<Double>>()o new HashMap<Integer, List<Long>>()o new HashMap<Integer, List<Number>>().

En pocas palabras, estás tratando de hacer trampa con el compilador y se defiende bastante contra tal trampa.

NB: la respuesta publicada por Maurice Perry es absolutamente correcta. Simplemente no estoy seguro de que sea lo suficientemente claro, así que intenté (realmente espero haberlo logrado) para agregar una publicación más extensa.

Makhno
fuente