Quand les génériques Java nécessitent-ils <? étend T> au lieu de <T> et y a-t-il un inconvénient à passer?

205

Dans l'exemple suivant (en utilisant JUnit avec des matchers Hamcrest):

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

Cela ne se compile pas avec la assertThatsignature de la méthode JUnit de:

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

Le message d'erreur du compilateur est:

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 change la assertThatsignature de la méthode en:

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

Ensuite, la compilation fonctionne.

Donc trois questions:

  1. Pourquoi exactement la version actuelle ne compile-t-elle pas? Bien que je comprenne vaguement les problèmes de covariance ici, je ne pourrais certainement pas l'expliquer si je le devais.
  2. Y a-t-il un inconvénient à changer la assertThatméthode Matcher<? extends T>? Y a-t-il d'autres cas qui se briseraient si vous faisiez cela?
  3. Y a-t-il un intérêt à la générisation de la assertThatméthode dans JUnit? La Matcherclasse ne semble pas l'exiger, car JUnit appelle la méthode matches, qui n'est pas typée avec un générique, et ressemble simplement à une tentative de forcer une sécurité de type qui ne fait rien, car la Matchervolonté n'est tout simplement pas en fait correspond et le test échouera malgré tout. Aucune opération dangereuse n'est impliquée (ou du moins il semble).

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());
    }
}
Yishai
la source
ce lien est très utile (génériques, héritage et sous-type): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Réponses:

145

Tout d'abord - je dois vous diriger vers http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - elle fait un travail incroyable.

L'idée de base est que vous utilisez

<T extends SomeClass>

quand le paramètre réel peut être SomeClassou 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 expectedpeut contenir des objets Class qui représentent n'importe quelle classe qui implémente Serializable. Votre carte de résultat indique qu'elle ne peut contenir que Datedes objets de classe.

Lorsque vous passez dans le résultat, vous définissez Texactement Mapde Stringla Dateobjets de classe, qui ne correspond pas à Mapdes Stringà tout ce qui est Serializable.

Une chose à vérifier - êtes-vous sûr de vouloir Class<Date>et non Date? Une carte de Stringto Class<Date>ne semble pas terriblement utile en général (elle ne peut contenir Date.classque des valeurs plutôt que des instances de Date)

En ce qui concerne la générisation assertThat, l'idée est que la méthode peut garantir qu'un Matchermodèle qui correspond au type de résultat est transmis.

Scott Stanchfield
la source
Dans ce cas, oui, je veux une carte des classes. L'exemple que j'ai donné est conçu pour utiliser des classes JDK standard plutôt que mes classes personnalisées, mais dans ce cas, la classe est en fait instanciée par réflexion et utilisée en fonction de la clé. (Une application distribuée où le client n'a pas les classes de serveur disponibles, juste la clé de la classe à utiliser pour effectuer le travail côté serveur).
Yishai
6
Je suppose que mon cerveau est bloqué sur la raison pour laquelle une carte contenant des classes de type Date ne s'intègre pas bien dans un type de cartes contenant des classes de type Serializable. Bien sûr, les classes de type Serializable pourraient également être d'autres classes, mais cela inclut certainement le type Date.
Yishai
En affirmant que pour s'assurer que le casting est effectué pour vous, la méthode matcher.matches () s'en fiche, donc comme le T n'est jamais utilisé, pourquoi l'impliquer? (le type de retour de la méthode est nul)
Yishai
Ahhh - c'est ce que j'obtiens pour ne pas avoir lu le def de l'assertThat assez près. On dirait que c'est seulement pour s'assurer qu'un Matcher convenable est passé ...
Scott Stanchfield
28

Merci à tous ceux qui ont répondu à la question, cela m'a vraiment aidé à clarifier les choses. À la fin, la réponse de Scott Stanchfield a été la plus proche de la façon dont j'ai fini par la comprendre, mais comme je ne l'ai pas compris quand il l'a écrite pour la première fois, j'essaie de reformuler le problème afin que, espérons-le, quelqu'un d'autre en profite.

Je vais reformuler la question en termes de liste, car il n'a qu'un seul paramètre générique et cela le rendra plus facile à comprendre.

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 d'avoir le compilateur garantissant que cela est sûr (pas d'exceptions d'exécution).

Prenons le cas de List. L'essence de ma question est pourquoi une méthode qui prend un type T et une liste n'acceptera pas une liste de quelque chose plus bas 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 sera pas compilé, car le paramètre de liste est une liste de dates, pas une liste de chaînes. Les génériques ne seraient pas très utiles si cela se compilait.

La même chose s'applique à une carte <String, Class<? extends Serializable>>Ce n'est pas la même chose qu'une carte <String, Class<java.util.Date>>. Ils ne sont pas covariants, donc si je voulais prendre une valeur de la carte contenant des classes de date et la mettre dans la carte contenant des éléments sérialisables, c'est bien, mais une signature de méthode qui dit:

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

Veut pouvoir 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 réellement de ces choses, la signature de la méthode nécessite la covariance, qu'elle n'obtient pas, donc elle ne compile pas.

Sur la deuxième question,

Matcher<? extends T>

Aurait l'inconvénient d'accepter vraiment n'importe quoi lorsque T est un objet, ce qui n'est pas l'intention des API. Le but est de garantir statiquement que le matcher correspond à 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 contrôlées (il n'y aurait pas de transtypage dangereux dans l'API JUnit si cette méthode n'était pas générique), mais ils essaient d'accomplir autre chose - s'assurer statiquement que le deux paramètres sont susceptibles de correspondre.

EDIT (après de nouvelles réflexions et expériences):

L'un des gros problèmes avec la signature de la méthode assertThat est la tentative d'assimiler une variable T à un paramètre générique de T. Cela ne fonctionne pas, car ils ne sont pas covariants. Ainsi, par exemple, vous pouvez avoir un T qui est un List<String>mais passer ensuite une correspondance à laquelle le compilateur travaille Matcher<ArrayList<T>>. Maintenant, si ce n'était pas un paramètre de type, les choses iraient bien, car List et ArrayList sont covariantes, mais puisque Generics, en ce qui concerne le compilateur, nécessite ArrayList, il ne peut pas tolérer une liste pour des raisons que j'espère claires. de ce qui précède.

Yishai
la source
Je ne comprends toujours pas pourquoi je ne peux pas upcast cependant. Pourquoi ne puis-je pas transformer une liste de dates en une liste de sérialisables?
Thomas Ahle
@ThomasAhle, car alors les références qui pensent qu'il s'agit d'une liste de dates se heurteront à des erreurs de casting lorsqu'elles trouveront des chaînes ou tout autre sérialisable.
Yishai
Je vois, mais que se passe-t-il si je me débarrasse en quelque sorte de l'ancienne référence, comme si je renvoyais le à List<Date>partir d'une méthode de type List<Object>? Cela devrait être sûr, même si java ne le permet pas.
Thomas Ahle
14

Cela se résume à:

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 Class c1 pourrait contenir une instance Long (puisque l'objet sous-jacent à un moment donné aurait pu l'être List<Long>), mais ne peut évidemment pas être converti en une date car il n'y a aucune garantie que la classe "inconnue" était Date. Ce n'est pas typsesafe, donc le compilateur l'interdit.

Cependant, si nous introduisons un autre objet, disons 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 essentiellement 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 j'espère que cela explique pourquoi cela aide à comprendre les génériques en regardant comment vous pouvez vous attribuer des références génériques. C'est également plus déroutant puisque le compilateur déduit le type de T lorsque vous effectuez l'appel de fonction (vous ne dites pas explicitement que c'était T).

GreenieMeanie
la source
9

La raison pour laquelle votre code d'origine ne compile pas est que <? extends Serializable>cela ne signifie pas «toute classe qui étend Serializable», mais «une classe inconnue mais spécifique qui étend Serializable».

Par exemple, étant donné le code tel qu'il est écrit, il est tout à fait valable d'attribuer new TreeMap<String, Long.class>()>à expected. Si le compilateur permettait au code de compiler, le assertThat()serait probablement rompu car il attendrait des Dateobjets au lieu des Longobjets qu'il trouve dans la carte.

erickson
la source
1
Je ne suis pas tout à fait en train de suivre - quand vous dites "ne veut pas dire ... mais ...": quelle est la différence? (comme, quel serait un exemple d'une classe "connue mais non spécifique" qui correspond à la première définition mais pas à la dernière?)
poundifdef
Oui, c'est un peu gênant; Je ne sais pas comment mieux l'exprimer ... est-il plus logique de dire: "'?' est un type inconnu, pas un type qui correspond à quelque chose? "
erickson
1
Ce qui peut aider à l'expliquer, c'est que pour "toute classe qui étend Serializable", vous pouvez simplement utiliser<Serializable>
c0der
8

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 donnée peut "avoir", mais le type d'autres références génériques avec lesquelles il est compatible (cela peut sembler déroutant ...) En tant que tel, 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 ou une sous-classe de Serializable. N'y pensez PAS en termes de LISTE UNIQUE pouvant contenir des sous-classes de Sérialisable (car cela est une sémantique incorrecte et conduit à une mauvaise compréhension des génériques).

GreenieMeanie
la source
Cela aide certainement, mais le terme «peut sembler déroutant» est en quelque sorte remplacé par un «bruit déroutant». À titre de suivi, alors pourquoi, selon cette explication, la méthode avec Matcher se <? extends T>compile-t-elle?
Yishai
si nous définissons comme List <Serializable>, cela fait la même chose non? Je parle de votre 2ème para. c'est-à-dire que le polymorphisme le gérerait?
Supun Wijerathne
3

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.Collectionspropose 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, la liste peut, bien sûr, contenir des instances de types qui s'étendent T. Si la liste contient des animaux, la liste 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 trier en fonction de ces propriétés particulières aux sous-classes de T, comment pouvons-nous nous attendre à ce que cette méthode fasse cela? Une limitation 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 un T, alors c'est aussi une instance des superclasses de T. Par conséquent, il nous permet d'utiliser un comparateur de Tou n'importe quelle superclasse de T, ie ? super T.

Lucas Ross
la source
1

et si vous utilisez

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
la source
Oui, c'est un peu ce que ma réponse ci-dessus voulait dire.
GreenieMeanie
Non, cela n'aide pas la situation, du moins la façon dont j'ai essayé.
Yishai