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 assertThat
firma 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 assertThat
firma del método a:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Entonces la compilación funciona.
Entonces tres preguntas:
- ¿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.
- ¿Hay algún inconveniente en cambiar el
assertThat
método aMatcher<? extends T>
? ¿Hay otros casos que se romperían si hicieras eso? - ¿Tiene algún sentido la genéricaización del
assertThat
método en JUnit? LaMatcher
clase 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, yaMatcher
que 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());
}
}
Respuestas:
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
cuando el parámetro real puede ser
SomeClass
o cualquier subtipo de él.En tu ejemplo
Estás diciendo que
expected
puede contener objetos de clase que representan cualquier clase que implementeSerializable
. Su mapa de resultados dice que solo puede contenerDate
objetos de clase.Cuando se pasa en consecuencia, se está configurando
T
a exactamenteMap
deString
queDate
los objetos de clase, que no coincideMap
deString
cualquier cosa que seSerializable
.Una cosa para verificar: ¿estás seguro de que quieres
Class<Date>
y noDate
? Un mapa deString
toClass<Date>
no suena terriblemente útil en general (todo lo que puede contener esDate.class
como valores en lugar de instancias deDate
)En cuanto a la genéricaización
assertThat
, la idea es que el método pueda garantizar queMatcher
se pase una que se ajuste al tipo de resultado.fuente
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:
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:Quiere poder hacer ambas cosas:
y
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,
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 trabajaMatcher<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.fuente
List<Date>
de un método con tipoList<Object>
? Eso debería ser seguro, incluso si Java no lo permite.Se reduce a:
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:
... Sin embargo, si el tipo de la Lista se convierte? extiende T en lugar de T ....
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).
fuente
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>()>
aexpected
. Si el compilador permitiera que el código se compilara,assertThat()
presumiblemente se rompería porque esperaríaDate
objetos en lugar de losLong
objetos que encuentra en el mapa.fuente
<Serializable>
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).fuente
<? extends T>
compila el método con Matcher ?Sé que esta es una vieja pregunta, pero quiero compartir un ejemplo que creo que explica bastante bien los comodines delimitados.
java.util.Collections
ofrece este método:Si tenemos una Lista de
T
, la Lista puede, por supuesto, contener instancias de tipos que se extiendenT
. 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 deT
, ¿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 aComparator<T>
haría que este método sea utilizable. Pero, el creador de este método reconoció que si algo es unT
, entonces también es una instancia de las superclases deT
. Por lo tanto, nos permite usar un Comparador deT
o cualquier superclase deT
, es decir? super T
.fuente
que pasa si usas
fuente