¿Cuándo los generics de Java requieren en lugar de y ¿hay algún inconveniente de cambio?

Dado el siguiente ejemplo (usando JUnit con Hamcrest matchers):

Map<String, Class> expected = null; Map<String, Class> result = null; assertThat(result, is(expected)); 

Esto no se comstack con la firma JUnit assertThat de:

 public static  void assertThat(T actual, Matcher matcher) 

El mensaje de error del comstackdor es:

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

Sin embargo, si cambio la firma de assertThat a:

 public static  void assertThat(T result, Matcher matcher) 

Entonces la comstackción funciona.

Entonces tres preguntas:

  1. ¿Por qué exactamente no comstack la versión actual? Aunque entiendo vagamente los problemas de covarianza aquí, ciertamente no podría explicarlo si tuviera que hacerlo.
  2. ¿Hay alguna desventaja en cambiar el método assertThat a Matcher Matcher ? ¿Hay otros casos que se romperían si hicieras eso?
  3. ¿Hay algún punto para la genérica del método assertThat en JUnit? La clase Matcher no parece requerirlo, ya que JUnit llama al método matches, que no está tipeado con ningún genérico, y simplemente parece un bash de forzar un tipo de seguridad que no hace nada, ya que el Matcher 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  void assertThat(T actual, Matcher matcher) { assertThat("", actual, matcher); } public static  void assertThat(String reason, T actual, Matcher 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()); } } 

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

  

cuando el parámetro real puede ser SomeClass o cualquier subtipo de este.

En tu ejemplo,

 Map> expected = null; Map> 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 y no Date ? Un mapa de String to Class 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.

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 o Map como en el ejemplo) es forzar un downcast y hacer que el comstackdor 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 dateList = new ArrayList(); Serializable s = new String(); addGeneric(s, dateList); .... private  void addGeneric(T element, List list) { list.add(element); } 

Esto no se comstackrá, porque el parámetro lista es una lista de fechas, no una lista de cadenas. Los generics no serían muy útiles si esto se comstackra.

Lo mismo se aplica a un mapa > > No es lo mismo que un Map > . 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  void genericAdd(T value, List 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 comstack.

En la segunda pregunta,

 Matcher 

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 pero luego pase una coincidencia que el comstackdor resuelva en Matcher> . 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 comstackdor, requiere ArrayList, no puede tolerar una lista por razones que espero sean claras. de lo de arriba

Se reduce a:

 Class c1 = null; Class 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 ), 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 comstackdor 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> l1 = null; List> 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> l1 = null; List> l2 = null; l1 = l2; // compiles l2 = l1; // won't compile 

Creo que al cambiar Matcher to Matcher Matcher to Matcher , básicamente está introduciendo el escenario similar a la asignación de l1 = l2;

Todavía es muy confuso tener nesteds comodines, pero espero que tenga sentido en cuanto a por qué ayuda a comprender los generics al ver cómo se pueden asignar referencias genéricas entre sí. También es aún más confuso ya que el comstackdor está deduciendo el tipo de T cuando realiza la llamada de función (no está diciendo explícitamente que era T).

La razón por la cual tu código original no comstack es ese 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()> a lo expected . Si el comstackdor permitió la comstackció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.

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 List 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).

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  void sort(List list, Comparator 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 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

que tal si usas

 Map> expected = null;