¿Cuándo requieren los genéricos de Java en lugar de y ¿hay algún inconveniente de la conmutación?


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

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

Esto no compila con la firma del método 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 del método assertThat a:

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

Entonces la compilación funciona.

Así que tres preguntas:

  1. ¿Por qué exactamente no compila la versión actual? Aunque entiendo vagamente los problemas de covarianza aquí, yo ciertamente no podría explicarlo si tuviera que hacerlo.
  2. ¿Hay algún inconveniente en cambiar el método assertThat a Matcher<? extends T>? ¿Hay otros casos que se romperían si hicieras eso?
  3. ¿Tiene algún sentido generalizar el método assertThat en JUnit? La clase Matcher no parece requerirla, ya que JUnit llama al método matches, que no se escribe con ningún genérico, y solo parece un intento de forzar una seguridad de tipo que no hace nada, ya que el Matcher simplemente no coincidirá, y la prueba fallará de todos modos. No hay operaciones inseguras involucradas (o eso parece).

Para 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());
    }
}
Author: blacktide, 2009-05-22

7 answers

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

La idea básica es que uses{[24]]}

<T extends SomeClass>

Cuando el parámetro real puede ser SomeClass o cualquier subtipo de ella.

En su ejemplo,

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

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

Cuando pase en resultado, está configurando T a Map exactamente de String a Date objetos de clase, que no coinciden con Map de String a nada que sea Serializable.

Una cosa a comprobar are ¿estás seguro de que quieres Class<Date> y no Date? Un mapa de String a 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 generalización assertThat, la idea es que el método puede garantizar que se pase un Matcher que se ajuste al tipo de resultado en.

 114
Author: Scott Stanchfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-10 00:44:20

Gracias a todos los que respondieron a 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éndolo, pero como no lo entendí cuando lo escribió por primera vez, estoy tratando de replantear el problema para que esperemos 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 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).

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 compilará, porque el parámetro list 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 diga:

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

Quiere ser capaz de hacer ambas cosas:

T x = list.get(0);

Y

list.add(value);

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

Sobre la segunda cuestión,{[12]]}

Matcher<? extends T>

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

La respuesta a la tercera la pregunta es que nada se perdería, en términos de funcionalidad sin verificar (no habría encasillamiento inseguro dentro de la API de JUnit si este método no se generalizara), pero están tratando de lograr algo más: asegurarse estáticamente de que los dos parámetros probablemente coincidan.

EDITAR (después de más contemplación y experiencia):

Uno de los grandes problemas con la firma del método assertThat es los intentos de equiparar una variable T con un parámetro genérico de T. Que no funciona, porque no son covariantes. Así, por ejemplo, puede tener una T que es un List<String> pero luego pasar una coincidencia que el compilador trabaja a Matcher<ArrayList<T>>. Ahora, si no fuera un parámetro de tipo, las cosas estarían bien, porque List y ArrayList son covariantes, pero ya que los genéricos, en lo que respecta al compilador require ArrayList, no puede tolerar una Lista por razones que espero sean claras de lo anterior.

 22
Author: Yishai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-30 14:07:35

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 Date. No es typsesafe, por lo que el compilador no lo permite.

Sin embargo, si introducimos algún otro objeto, digamos List (en su ejemplo este objeto es Matcher), entonces lo siguiente se convierte en verdadero:

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

...Obstante, 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ás introduciendo el escenario similar a asignar l1 = l2;

Sigue siendo muy confuso tener comodines anidados, pero espero que eso tenga sentido en cuanto a por qué ayuda a entender los genéricos mirando cómo se pueden asignar referencias genéricas entre sí. También es más confuso ya que el compilador está inferiendo el tipo de T cuando haces la llamada a la función (eres no diciendo explícitamente que era T es).

 11
Author: GreenieMeanie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-22 17:29:55

La razón por la que su código original no se compila es que <? 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 expected. Si el compilador permitía compilar el código, el assertThat() presumiblemente se rompería porque esperaría objetos Date en lugar de los objetos Long que encuentra en el mapa.

 8
Author: erickson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-02 08:08:06

Una forma de entender los comodines es pensar que el comodín no especifica el tipo de los posibles objetos que una referencia genérica dada puede "tener", 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 un tipo desconocido que es o una subclase de Serializable. NO piénsalo en términos de UNA SOLA LISTA capaz de contener subclases de Serializable (porque eso es una semántica incorrecta y conduce a un malentendido de los genéricos).

 6
Author: GreenieMeanie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-22 15:58:17

Sé que esta es una pregunta antigua, pero quiero compartir un ejemplo que creo que explica los comodines acotados bastante bien. 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 se 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 a subclases de T, ¿cómo podemos esperar que este método haga eso? Una limitación del Comparador 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 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 T o cualquier superclase de T, es decir, ? super T.

 3
Author: Lucas Ross,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-24 07:03:01

Y si usa

Map<String, ? extends Class<? extends Serializable>> expected = null;
 1
Author: newacct,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-22 17:30:03