web-dev-qa-db-fra.com

Quand Java génériques a-t-il besoin de <? étend T> au lieu de <T> et existe-t-il un inconvénient à la commutation?

Étant donné l'exemple suivant (en utilisant JUnit avec des adaptateurs Hamcrest):

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

Ceci ne compile pas avec la signature de méthode JUnit assertThat de:

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

Le message d'erreur du compilateur est le suivant:

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

Cependant, si je remplace la signature de méthode assertThat par:

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

Ensuite, la compilation fonctionne.

Donc trois questions:

  1. Pourquoi la version actuelle ne compile-t-elle pas exactement? Bien que je comprenne vaguement les problèmes de covariance ici, je ne pourrais certainement pas l'expliquer si je devais le faire.
  2. Existe-t-il un inconvénient à ce que la méthode assertThat soit remplacée par Matcher<? extends T>? Y a-t-il d'autres cas qui se briseraient si vous le faisiez?
  3. Est-il utile de généraliser la méthode assertThat dans JUnit? La classe Matcher ne semble pas en avoir besoin, car JUnit appelle la méthode match, qui n’est typée avec aucun générique, et qui ressemble à une tentative de forcer une sécurité de type qui ne fait rien, comme le Matcher ne correspondra pas, et le test échouera quand même. Aucune opération dangereuse impliquée (ou semble-t-il).

Pour référence, voici l'implémentation 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());
    }
}
186
Yishai

Premièrement - je dois vous diriger vers http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - elle fait un travail extraordinaire.

L'idée de base est que vous utilisez

<T extends SomeClass>

lorsque le paramètre réel peut être SomeClass ou n’importe quel sous-type de celui-ci.

Dans votre exemple

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

Vous dites que expected peut contenir des objets de classe qui représentent n'importe quelle classe implémentant Serializable. Votre carte de résultat indique qu'elle ne peut contenir que des objets de classe Date.

Lorsque vous transmettez un résultat, vous définissez T sur Map exactement de String sur Date objets de classe, qui ne correspond pas à Map de String à tout ce qui est Serializable.

Une chose à vérifier - êtes-vous sûr de vouloir Class<Date> et non Date? Une carte de String à Class<Date> ne semble pas très utile en général (elle ne peut contenir que Date.class sous forme de valeurs plutôt que d'instances de Date)

Pour ce qui est de la générique assertThat, l’idée est que la méthode peut garantir qu’un Matcher correspondant au type de résultat est passé.

128

Merci à tous ceux qui ont répondu à la question, cela m'a vraiment aidé à clarifier les choses. En fin de compte, la réponse de Scott Stanchfield était très proche de la façon dont j'avais fini par comprendre, mais comme je ne le comprenais pas lorsqu'il l'a écrite pour la première fois, j'essaie de reformuler le problème afin que quelqu'un d'autre en profite.

Je vais reformuler la question en termes de liste, car elle ne comporte qu'un seul paramètre générique, ce qui facilitera la compréhension.

Le but de la classe paramétrée (telle que List<Date> ou Map<K, V> comme dans l'exemple) est de forcer un downcast et de demander au compilateur de garantir sa sécurité (pas d'exécution). exceptions).

Prenons le cas de la liste. L'essence de ma question est de savoir pourquoi une méthode qui prend un type T et une liste n'acceptera pas une liste de quelque chose plus loin dans la chaîne d'héritage que T. Considérons cet exemple artificiel:

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);
}

Cela ne compilera pas, car le paramètre list est une liste de dates, pas une liste de chaînes. Les génériques ne seraient pas très utiles si cela compilait.

La même chose s'applique à un Map<String, Class<? extends Serializable>> Ce n'est pas la même chose qu'un Map<String, Class<Java.util.Date>>. Elles ne sont pas des covariantes. Par conséquent, si je voulais prendre une valeur de la carte contenant les classes de date et la placer dans la carte contenant des éléments sérialisables, c'est très bien, mais une signature de méthode qui dit:

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

Veut être capable de faire les deux:

T x = list.get(0);

et

list.add(value);

Dans ce cas, même si la méthode junit ne se soucie pas vraiment de ces choses, la signature de la méthode nécessite la covariance, qu'elle ne reçoit pas, donc elle ne compile pas.

Sur la deuxième question,

Matcher<? extends T>

Aurait l'inconvénient d'accepter quoi que ce soit réellement lorsque T est un objet, ce qui n'est pas l'intention des API. L'intention est de s'assurer de manière statique que l'adaptateur corresponde à l'objet réel, et il n'y a aucun moyen d'exclure Object de ce calcul.

La réponse à la troisième question est que rien ne serait perdu, en termes de fonctionnalités non vérifiées (il n'y aurait pas de conversion de typage non sécurisée au sein de l'API JUnit si cette méthode n'était pas généralisée), mais ils essayaient d'accomplir autre chose - assurer statiquement que le deux paramètres sont susceptibles de correspondre.

EDIT (après réflexion et expérience):

L'un des gros problèmes de la signature de la méthode assertThat réside dans les tentatives d'assimilation d'une variable T à un paramètre générique de T. Cela ne fonctionne pas, car elles ne sont pas covariantes. Ainsi, par exemple, vous pouvez avoir un T qui est un List<String> mais transmettez ensuite une correspondance que le compilateur calcule à Matcher<ArrayList<T>>. Maintenant, si ce n'était pas un paramètre de type, tout irait bien, car List et ArrayList sont covariants, mais puisque Generics, en ce qui concerne le compilateur nécessite ArrayList, il ne peut tolérer une liste pour des raisons que j'espère claires. de ci-dessus.

25
Yishai

Cela revient à:

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

Vous pouvez voir que la référence de classe c1 pourrait contenir une instance Longue (puisque l'objet sous-jacent à un moment donné aurait pu être List<Long>), mais ne peut évidemment pas être convertie en Date, car rien ne garantit que la classe "inconnue" était Date. Ce n'est pas typsesafe, donc le compilateur l'interdit.

Cependant, si nous introduisons un autre objet, par exemple List (dans votre exemple, cet objet est Matcher), alors ce qui suit devient vrai:

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

... Cependant, si le type de la liste devient? étend T au lieu 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

Je pense qu'en changeant Matcher<T> to Matcher<? extends T>, vous introduisez fondamentalement le scénario similaire à l'affectation de l1 = l2;

Il est toujours très déroutant d’avoir des caractères génériques imbriqués, mais nous espérons que cela explique pourquoi il est utile de comprendre les génériques en examinant comment vous pouvez affecter des références génériques les unes aux autres. Cela est également source de confusion puisque le compilateur déduit le type de T lorsque vous appelez la fonction (vous n’indiquez pas explicitement qu’il s’agissait de T est).

14
GreenieMeanie

La raison pour laquelle votre code original n'est pas compilé est que <? extends Serializable> ne signifie pas, "toute classe qui étend Serializable," mais "une classe inconnue mais spécifique qui étend Serializable."

Par exemple, étant donné le code écrit, il est parfaitement correct d’attribuer new TreeMap<String, Long.class>()> à expected. Si le compilateur autorisait la compilation du code, la assertThat() se briserait probablement car elle attendrait des objets Date au lieu des objets Long qu'il trouve dans la carte.

8
erickson

Une façon pour moi de comprendre les caractères génériques est de penser que le caractère générique ne spécifie pas le type des objets possibles qu'une "référence générique" peut "avoir", mais le type des autres références génériques avec lesquelles il est compatible (cela peut paraître déroutant). ...) En tant que telle, la première réponse est très trompeuse dans son libellé.

En d'autres termes, List<? extends Serializable> signifie que vous pouvez affecter cette référence à d'autres listes où le type est un type inconnu qui est une sous-classe de Serializable. NE PENSEZ PAS cela en termes de A SINGLE LIST pouvant contenir des sous-classes de Serializable (car c'est une sémantique incorrecte et conduit à une incompréhension des génériques).

7
GreenieMeanie

Je sais que c’est une vieille question, mais je veux partager un exemple qui, je pense, explique assez bien les caractères génériques délimités. Java.util.Collections propose cette méthode:

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

Si nous avons une liste de T, celle-ci peut, bien sûr, contenir des instances de types qui s'étendent de T. Si la liste contient des animaux, elle peut contenir à la fois des chiens et des chats (les deux animaux). Les chiens ont une propriété "woofVolume" et les chats ont une propriété "meowVolume". Alors que nous aimerions peut-être trier sur la base de ces propriétés particulières aux sous-classes de T, comment pouvons-nous espérer que cette méthode le fasse? Une limite de Comparator est qu'il ne peut comparer que deux choses d'un seul type (T). Donc, exiger simplement un Comparator<T> rendrait cette méthode utilisable. Mais le créateur de cette méthode a reconnu que si quelque chose est une T, alors c'est aussi une instance des superclasses de T. Par conséquent, il nous permet d’utiliser un comparateur de T ou n’importe quelle superclasse de T, c’est-à-dire ? super T.

3
Lucas Ross

et si vous utilisez

Map<String, ? extends Class<? extends Serializable>> expected = null;
1
newacct