¿Cuándo requieren los genéricos de Java <? extiende T> en lugar de <T> y ¿hay alguna desventaja de cambiar?

205

Dado el siguiente ejemplo (usando JUnit con los emparejadores Hamcrest):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

Esto no se compila con la assertThatfirma del método JUnit de:

public static <T> void assertThat(T actual, Matcher<T> matcher)

El mensaje de error del compilador es:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

Sin embargo, si cambio la assertThatfirma del método a:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Entonces la compilación funciona.

Entonces tres preguntas:

  1. ¿Por qué no se compila exactamente la versión actual? Aunque entiendo vagamente los problemas de covarianza aquí, ciertamente no podría explicarlo si tuviera que hacerlo.
  2. ¿Hay algún inconveniente en cambiar el assertThatmétodo a Matcher<? extends T>? ¿Hay otros casos que se romperían si hicieras eso?
  3. ¿Tiene algún sentido la genéricaización del assertThatmétodo en JUnit? La Matcherclase no parece requerirlo, ya que JUnit llama al método de coincidencias, que no está escrito con ningún genérico, y solo parece un intento de forzar una seguridad de tipo que no hace nada, ya Matcherque en realidad no lo hará. coinciden, y la prueba fallará independientemente. No hay operaciones inseguras involucradas (o eso parece).

Como referencia, aquí está la implementación JUnit de assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}
Yishai
fuente
Este enlace es muy útil (genéricos, herencia y subtipo): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Respuestas:

145

Primero, tengo que dirigirlo a http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html , ella hace un trabajo increíble.

La idea básica es que usas

<T extends SomeClass>

cuando el parámetro real puede ser SomeClasso cualquier subtipo de él.

En tu ejemplo

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

Estás diciendo que expectedpuede contener objetos de clase que representan cualquier clase que implemente Serializable. Su mapa de resultados dice que solo puede contenerDate objetos de clase.

Cuando se pasa en consecuencia, se está configurando Ta exactamente Mapde Stringque Datelos objetos de clase, que no coincide Mapde Stringcualquier cosa que se Serializable.

Una cosa para verificar: ¿estás seguro de que quieres Class<Date>y no Date? Un mapa de Stringto Class<Date>no suena terriblemente útil en general (todo lo que puede contener es Date.classcomo valores en lugar de instancias de Date)

En cuanto a la genéricaización assertThat, la idea es que el método pueda garantizar que Matcherse pase una que se ajuste al tipo de resultado.

Scott Stanchfield
fuente
En este caso, sí, quiero un mapa de clases. El ejemplo que di es ideado para usar clases JDK estándar en lugar de mis clases personalizadas, pero en este caso la clase se instancia a través de la reflexión y se usa en función de la clave. (Una aplicación distribuida donde el cliente no tiene las clases de servidor disponibles, solo la clave de qué clase usar para hacer el trabajo del lado del servidor).
Yishai
66
Supongo que mi cerebro está atorado en por qué un Mapa que contiene clases de tipo Fecha no encaja perfectamente en un tipo de Mapas que contienen clases de tipo Serializable. Claro que las clases de tipo Serializable también podrían ser otras clases, pero ciertamente incluye el tipo Fecha.
Yishai
En la afirmación de que asegurarse de que el reparto se realice para usted, el método matcher.matches () no le importa, por lo que, dado que la T nunca se usa, ¿por qué involucrarla? (el tipo de devolución del método es nulo)
Yishai
Ahhh, eso es lo que obtengo por no leer la definición de la afirmación, lo suficientemente cerca. Parece que es sólo para asegurarse de que un Matcher ajuste se pasa en ...
a Scott Stanchfield
28

Gracias a todos los que respondieron la pregunta, realmente me ayudó a aclarar las cosas. Al final, la respuesta de Scott Stanchfield se acercó más a cómo terminé entendiéndola, pero como no lo entendí cuando la escribió por primera vez, estoy tratando de reformular el problema para que alguien más se beneficie.

Voy a replantear la pregunta en términos de Lista, ya que solo tiene un parámetro genérico y eso hará que sea más fácil de entender.

El propósito de la clase parametrizada (como List <Date>o Map <K, V>como en el ejemplo) es forzar un downcast y hacer que el compilador garantice que esto es seguro (sin excepciones de tiempo de ejecución).

Considere el caso de List. La esencia de mi pregunta es por qué un método que toma un tipo T y una Lista no aceptará una Lista de algo más abajo en la cadena de herencia que T. Considere este ejemplo artificial:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

Esto no se compilará, porque el parámetro de lista es una lista de fechas, no una lista de cadenas. Los genéricos no serían muy útiles si esto compilara.

Lo mismo se aplica a un Mapa. <String, Class<? extends Serializable>>No es lo mismo que un Mapa <String, Class<java.util.Date>>. No son covariantes, así que si quisiera tomar un valor del mapa que contiene clases de fecha y ponerlo en el mapa que contiene elementos serializables, está bien, pero una firma de método que dice:

private <T> void genericAdd(T value, List<T> list)

Quiere poder hacer ambas cosas:

T x = list.get(0);

y

list.add(value);

En este caso, aunque el método junit en realidad no se preocupa por estas cosas, la firma del método requiere la covarianza, que no está obteniendo, por lo tanto, no se compila.

Sobre la segunda pregunta,

Matcher<? extends T>

Tendría la desventaja de aceptar realmente algo cuando T es un Objeto, que no es la intención de las API. La intención es asegurar estáticamente que el emparejador coincida con el objeto real, y no hay forma de excluir Objeto de ese cálculo.

La respuesta a la tercera pregunta es que no se perdería nada, en términos de funcionalidad no verificada (no habría una conversión de tipos insegura dentro de la API JUnit si este método no se generizara), pero están tratando de lograr algo más: asegúrese estáticamente de que Es probable que dos parámetros coincidan.

EDITAR (después de una mayor contemplación y experiencia):

Uno de los grandes problemas con la firma de aserción de ese método es intentar equiparar una variable T con un parámetro genérico de T. Eso no funciona, porque no son covariantes. Entonces, por ejemplo, puede tener una T que es un List<String>pero luego pasar una coincidencia con la que el compilador trabaja Matcher<ArrayList<T>>. Ahora, si no fuera un parámetro de tipo, las cosas estarían bien, porque List y ArrayList son covariantes, pero dado que Generics, en lo que respecta al compilador requiere ArrayList, no puede tolerar una Lista por razones que espero que estén claras De lo anterior.

Yishai
fuente
Sin embargo, todavía no entiendo por qué no puedo transmitir. ¿Por qué no puedo convertir una lista de fechas en una lista de serializables?
Thomas Ahle
@ThomasAhle, porque las referencias que piensan que es una lista de Fechas se encontrarán con errores de transmisión cuando encuentren Cadenas o cualquier otro serializable.
Yishai
Ya veo, pero ¿qué pasa si de alguna manera me deshago de la referencia anterior, como si devolviera el List<Date>de un método con tipo List<Object>? Eso debería ser seguro, incluso si Java no lo permite.
Thomas Ahle
14

Se reduce a:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

Puede ver que la referencia de Clase c1 podría contener una instancia Larga (ya que el objeto subyacente en un momento dado podría haber sido List<Long>), pero obviamente no se puede convertir a una Fecha ya que no hay garantía de que la clase "desconocida" fuera Fecha. No es typsesafe, por lo que el compilador no lo permite.

Sin embargo, si presentamos algún otro objeto, digamos Lista (en su ejemplo este objeto es Matcher), entonces lo siguiente se cumple:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Sin embargo, si el tipo de la Lista se convierte? extiende T en lugar de T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Creo que al cambiar Matcher<T> to Matcher<? extends T>, básicamente está introduciendo el escenario similar a asignar l1 = l2;

Todavía es muy confuso tener comodines anidados, pero con suerte eso tiene sentido por qué ayuda a comprender los genéricos al observar cómo pueden asignarse referencias genéricas entre sí. También es más confuso ya que el compilador deduce el tipo de T cuando realiza la llamada a la función (no está diciendo explícitamente que era T).

GreenieMeanie
fuente
9

La razón de su código original no hace compilación es que <? extends Serializable>hace no tanto, "cualquier clase que se extiende Serializable", pero "alguna clase específica desconocida, pero que se extiende Serializable."

Por ejemplo, teniendo en cuenta el código como está escrito, es perfectamente válido para asignar new TreeMap<String, Long.class>()>a expected. Si el compilador permitiera que el código se compilara, assertThat()presumiblemente se rompería porque esperaría Dateobjetos en lugar de los Longobjetos que encuentra en el mapa.

erickson
fuente
1
No estoy del todo siguiendo - cuando dices "no significa ... pero ...": ¿cuál es la diferencia? (como, ¿cuál sería un ejemplo de una clase "conocida pero no específica" que se ajuste a la primera definición pero no a la segunda?)
poundifdef
Sí, eso es un poco incómodo; no estoy seguro de cómo expresarlo mejor ... ¿tiene más sentido decir "'?' ¿Es un tipo desconocido, no un tipo que coincida con algo? "
erickson
1
Lo que puede ayudar a explicarlo es que para "cualquier clase que extienda Serializable" simplemente podría usar<Serializable>
c0der
8

Una forma de entender los comodines es pensar que el comodín no está especificando el tipo de los posibles objetos que puede "tener" una referencia genérica, sino el tipo de otras referencias genéricas con las que es compatible (esto puede sonar confuso ...) Como tal, la primera respuesta es muy engañosa en su redacción.

En otras palabras, List<? extends Serializable>significa que puede asignar esa referencia a otras Listas donde el tipo es algún tipo desconocido que es o una subclase de Serializable. NO lo piense en términos de UNA LISTA ÚNICA que pueda contener subclases de Serializable (porque eso es una semántica incorrecta y conduce a un malentendido de los Genéricos).

GreenieMeanie
fuente
Eso ciertamente ayuda, pero el "puede sonar confuso" es reemplazado por un "suena confuso". Como seguimiento, entonces, ¿por qué, de acuerdo con esta explicación, se <? extends T>compila el método con Matcher ?
Yishai
si definimos como Lista <Serializable>, ¿hace lo mismo? Estoy hablando de tu segundo párrafo. es decir, el polimorfismo lo manejaría?
Supun Wijerathne
3

Sé que esta es una vieja pregunta, pero quiero compartir un ejemplo que creo que explica bastante bien los comodines delimitados. java.util.Collectionsofrece este método:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

Si tenemos una Lista de T, la Lista puede, por supuesto, contener instancias de tipos que se extienden T. Si la lista contiene animales, la lista puede contener perros y gatos (ambos animales). Los perros tienen una propiedad "woofVolume" y los gatos tienen una propiedad "meowVolume". Si bien nos gustaría ordenar según estas propiedades particulares de las subclases de T, ¿cómo podemos esperar que este método lo haga? Una limitación de Comparator es que solo puede comparar dos cosas de un solo tipo ( T). Por lo tanto, requerir simplemente a Comparator<T>haría que este método sea utilizable. Pero, el creador de este método reconoció que si algo es un T, entonces también es una instancia de las superclases de T. Por lo tanto, nos permite usar un Comparador de To cualquier superclase de T, es decir ? super T.

Lucas Ross
fuente
1

que pasa si usas

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
fuente
Sí, esto es algo de lo que estaba conduciendo mi respuesta anterior.
GreenieMeanie
No, eso no ayuda a la situación, al menos como lo intenté.
Yishai