¿Cuándo los genéricos de Java requieren<? extiende T> en lugar de<T> y ¿hay algún inconveniente de cambio?
generics junit (7)
Gracias a todos los que respondieron la pregunta, realmente me ayudó a aclarar las cosas. Al final, la respuesta de Scott Stanchfield fue la más cercana a cómo terminé entendiéndolo, pero como no lo entendí cuando lo escribió por primera vez, estoy tratando de replantear el problema para que, con suerte, 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 lo hará 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).
Considera 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 lista es una lista de fechas, no una lista de cadenas. Los genéricos no serían muy útiles si esto se compilara.
Lo mismo se aplica a un mapa <String, Class<? extends Serializable>>
<String, Class<? extends Serializable>>
No es lo mismo que un Map <String, Class<java.util.Date>>
. No son covariantes, así que si quería 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.
En la segunda pregunta,
Matcher<? extends T>
Tendría la desventaja de aceptar cualquier cosa cuando T es un Objeto, que no es el propósito de la API. La intención es asegurar estáticamente que el emparejador coincida con el objeto real, y no hay forma de excluir el Objeto de ese cálculo.
La respuesta a la tercera pregunta es que no se perderá nada, en términos de funcionalidad sin marcar (no habría una conversión insegura dentro de la API de JUnit si este método no estuviera genérico), pero están tratando de lograr algo más: garantizar estáticamente que el dos parámetros es probable que coincidan.
EDITAR (después de más contemplación y experiencia):
Uno de los grandes problemas con la afirmación de que la firma del 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 sea una List<String>
pero luego pase una coincidencia que el compilador resuelva en Matcher<ArrayList<T>>
. Ahora bien, si no fuera un parámetro de tipo, las cosas estarían bien, porque List y ArrayList son covariantes, pero desde Generics, en lo que respecta al compilador, requiere ArrayList, no puede tolerar una lista por razones que espero sean claras. de lo de arriba
Dado el siguiente ejemplo (usando JUnit con Hamcrest matchers):
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 firma JUnit assertThat
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 firma de assertThat
a:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Entonces la compilación funciona.
Entonces tres preguntas:
- ¿Por qué exactamente no compila la versión actual? Aunque entiendo vagamente los problemas de covarianza aquí, ciertamente no podría explicarlo si tuviera que hacerlo.
- ¿Hay alguna desventaja en cambiar el método
assertThat
aMatcher<? extends T>
Matcher<? extends T>
? ¿Hay otros casos que se romperían si hicieras eso? - ¿Hay algún punto para la genérica del método
assertThat
en JUnit? La claseMatcher
no parece requerirlo, ya que JUnit llama al método matches, que no está tipeado con ningún genérico, y simplemente parece un intento de forzar un tipo de seguridad que no hace nada, ya que elMatcher
simplemente no lo hará. de hecho, coinciden, y la prueba fallará independientemente. No hay operaciones inseguras involucradas (o eso parece).
Como referencia, aquí está la implementación de 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());
}
}
La razón por la cual tu código original no compila es ese <? extends Serializable>
<? extends Serializable>
no significa, "cualquier clase que se extiende Serializable", sino "alguna clase desconocida pero específica que se extiende Serializable".
Por ejemplo, dado el código tal como está escrito, es perfectamente válido asignar new TreeMap<String, Long.class>()>
a lo expected
. Si el compilador permitió la compilación del código, el assertThat()
se rompería presumiblemente porque esperaría objetos Date
lugar de los objetos Long
que encuentra en el mapa.
Primero, tengo que dirigirte a http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - ella hace un trabajo increíble.
La idea básica es que uses
<T extends SomeClass>
cuando el parámetro real puede ser SomeClass
o cualquier subtipo de este.
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 se expected
que contenga objetos Class que representen cualquier clase que implemente Serializable
. Su mapa de resultados dice que solo puede contener objetos de la clase Date
.
Cuando pasa el resultado, está configurando T
para asignar exactamente objetos de la clase Map
of String
to Date
, que no coincide con Map
of String
a nada que sea Serializable
.
Una cosa para comprobar: ¿está seguro de que quiere Class<Date>
y no Date
? Un mapa de String
to Class<Date>
no suena terriblemente útil en general (todo lo que puede contener es Date.class
como valores en lugar de instancias de Date
)
En cuanto a la genérica assertThat
, la idea es que el método pueda garantizar que se transfiera un Matcher
que se ajuste al tipo de resultado.
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:
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 extienden T
Si la Lista contiene Animales, la Lista puede contener tanto Perros como Gatos (ambos Animales). Los perros tienen una propiedad "woofVolume" y los gatos tienen una propiedad "meowVolume". Si bien nos gustaría ordenar en función de 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 un Comparator<T>
haría que este método fuera utilizable. Pero, el creador de este método reconoció que si algo es una T
, entonces también es una instancia de las superclases de T
Por lo tanto, ¿nos permite usar un Comparador de T
o cualquier superclase de T
, es decir ? super T
? super T
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" 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 se cumple lo siguiente:
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 vez 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>
Matcher<T> to Matcher<? extends T>
, básicamente está introduciendo el escenario similar a la asignación de l1 = l2;
Todavía es muy confuso tener anidados comodines, pero espero que tenga sentido en cuanto a por qué ayuda a comprender los genéricos al ver cómo se pueden asignar referencias genéricas entre sí. También es aún más confuso ya que el compilador está deduciendo el tipo de T cuando realiza la llamada de función (no está diciendo explícitamente que era T).
Una forma de entender los comodines es pensar que el comodín no especifica el tipo de objetos posibles que la referencia genérica puede "tener", pero 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>
List<? extends Serializable>
significa que puede asignar esa referencia a otras Listas donde el tipo es un tipo desconocido que es o una subclase de Serializable. NO PIENSE en términos de UNA LISTA ÚNICA que sea capaz de mantener subclases de Serializable (porque esa es una semántica incorrecta y conduce a un malentendido de Genéricos).
que tal si usas
Map<String, ? extends Class<? extends Serializable>> expected = null;