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 assertThat
signature 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 assertThat
signature de la méthode en:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Ensuite, la compilation fonctionne.
Donc trois questions:
- 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.
- Y a-t-il un inconvénient à changer la
assertThat
méthodeMatcher<? extends T>
? Y a-t-il d'autres cas qui se briseraient si vous faisiez cela? - Y a-t-il un intérêt à la générisation de la
assertThat
méthode dans JUnit? LaMatcher
classe 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 laMatcher
volonté 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());
}
}
Réponses:
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
quand le paramètre réel peut être
SomeClass
ou n'importe quel sous-type de celui-ci.Dans votre exemple,
Vous dites que
expected
peut contenir des objets Class qui représentent n'importe quelle classe qui implémenteSerializable
. Votre carte de résultat indique qu'elle ne peut contenir queDate
des objets de classe.Lorsque vous passez dans le résultat, vous définissez
T
exactementMap
deString
laDate
objets de classe, qui ne correspond pas àMap
desString
à tout ce qui estSerializable
.Une chose à vérifier - êtes-vous sûr de vouloir
Class<Date>
et nonDate
? Une carte deString
toClass<Date>
ne semble pas terriblement utile en général (elle ne peut contenirDate.class
que des valeurs plutôt que des instances deDate
)En ce qui concerne la générisation
assertThat
, l'idée est que la méthode peut garantir qu'unMatcher
modèle qui correspond au type de résultat est transmis.la source
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:
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:Veut pouvoir faire les deux:
et
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,
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 travailleMatcher<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.la source
List<Date>
partir d'une méthode de typeList<Object>
? Cela devrait être sûr, même si java ne le permet pas.Cela se résume à:
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:
... Cependant, si le type de la liste devient? étend T au lieu de T ....
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).
la source
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, leassertThat()
serait probablement rompu car il attendrait desDate
objets au lieu desLong
objets qu'il trouve dans la carte.la source
<Serializable>
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).la source
<? extends T>
compile-t-elle?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:Si nous avons une liste de
T
, la liste peut, bien sûr, contenir des instances de types qui s'étendentT
. 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 deT
, 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 unComparator<T>
rendrait cette méthode utilisable. Mais, le créateur de cette méthode a reconnu que si quelque chose est unT
, alors c'est aussi une instance des superclasses deT
. Par conséquent, il nous permet d'utiliser un comparateur deT
ou n'importe quelle superclasse deT
, ie? super T
.la source
et si vous utilisez
la source