Anhand des folgenden Beispiels (Verwenden von JUnit mit Hamcrest-Matchern):
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));
Dies wird nicht mit der assertThat
Signatur der JUnit- Methode kompiliert :
public static <T> void assertThat(T actual, Matcher<T> matcher)
Die Compiler-Fehlermeldung lautet:
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>>>)
Wenn ich jedoch die assertThat
Methodensignatur in Folgendes ändere :
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Dann funktioniert die Zusammenstellung.
Also drei Fragen:
- Warum genau wird die aktuelle Version nicht kompiliert? Obwohl ich die Kovarianzprobleme hier vage verstehe, könnte ich es sicherlich nicht erklären, wenn ich müsste.
- Gibt es einen Nachteil bei der Änderung der
assertThat
Methode aufMatcher<? extends T>
? Gibt es andere Fälle, die brechen würden, wenn Sie das tun würden? - Hat die Generierung der
assertThat
Methode in JUnit irgendeinen Sinn ? DieMatcher
Klasse scheint dies nicht zu erfordern, da JUnit die Matches-Methode aufruft, die nicht mit einem Generikum typisiert ist und nur wie ein Versuch aussieht, eine Typensicherheit zu erzwingen, die nichts tut, wie dieMatcher
einfach nicht Übereinstimmung, und der Test wird trotzdem fehlschlagen. Keine unsicheren Operationen (oder so scheint es).
Als Referenz ist hier die JUnit-Implementierung von 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());
}
}
Antworten:
Erstens - ich muss Sie an http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html weiterleiten - sie macht einen tollen Job.
Die Grundidee ist, dass Sie verwenden
wenn der tatsächliche Parameter ein
SomeClass
beliebiger Subtyp sein kann.In Ihrem Beispiel
Sie sagen, das
expected
kann Klassenobjekte enthalten, die jede Klasse darstellen, die implementiert wirdSerializable
. Ihre Ergebniskarte sagt, dass es nur halten kannDate
Klassenobjekte enthalten kann.Wenn Sie in Folge passieren, du bist Einstellung
T
genauMap
derString
aufDate
Klassenobjekte, die nicht übereinMap
der ,String
dass die zu nichtsSerializable
.Eine Sache zu überprüfen - sind Sie sicher, dass Sie wollen
Class<Date>
und nichtDate
? Eine Karte vonString
toClass<Date>
klingt im Allgemeinen nicht besonders nützlich (alles, was sie enthalten kann, sindDate.class
Werte und keine Instanzen vonDate
).Bei der Generierung
assertThat
besteht die Idee darin, dass die Methode sicherstellen kann, dass einMatcher
zum Ergebnistyp passender Wert übergeben wird.quelle
Vielen Dank an alle, die die Frage beantwortet haben. Es hat mir wirklich geholfen, die Dinge zu klären. Am Ende kam die Antwort von Scott Stanchfield der Frage am nächsten, wie ich sie letztendlich verstanden habe, aber da ich ihn beim ersten Schreiben nicht verstanden habe, versuche ich, das Problem neu zu formulieren, damit hoffentlich jemand anderes davon profitiert.
Ich werde die Frage in Bezug auf List wiederholen, da sie nur einen generischen Parameter enthält und dadurch das Verständnis erleichtert wird.
Der Zweck der parametrisierten Klasse (wie z. B. Liste
<Date>
oder Karte<K, V>
wie im Beispiel) besteht darin , einen Downcast zu erzwingen und den Compiler garantieren zu lassen, dass dies sicher ist (keine Laufzeitausnahmen).Betrachten Sie den Fall von List. Das Wesentliche meiner Frage ist, warum eine Methode, die einen Typ T und eine Liste verwendet, keine Liste von etwas akzeptiert, das weiter unten in der Vererbungskette liegt als T. Betrachten Sie dieses erfundene Beispiel:
Dies wird nicht kompiliert, da der Listenparameter eine Liste von Datumsangaben und keine Liste von Zeichenfolgen ist. Generika wären nicht sehr nützlich, wenn dies kompiliert würde.
Das Gleiche gilt für eine Karte.
<String, Class<? extends Serializable>>
Es ist nicht dasselbe wie eine Karte<String, Class<java.util.Date>>
. Sie sind nicht kovariant. Wenn ich also einen Wert aus der Karte mit Datumsklassen in die Karte mit serialisierbaren Elementen einfügen möchte, ist das in Ordnung, aber eine Methodensignatur mit folgenden Angaben:Möchte beides können:
und
In diesem Fall erfordert die Methodensignatur die Kovarianz, die sie nicht erhält, obwohl sie sich nicht wirklich um diese Dinge kümmert, weshalb sie nicht kompiliert wird.
Zur zweiten Frage:
Hätte den Nachteil, wirklich etwas zu akzeptieren, wenn T ein Objekt ist, was nicht die Absicht der APIs ist. Es soll statisch sichergestellt werden, dass der Matcher mit dem tatsächlichen Objekt übereinstimmt, und es gibt keine Möglichkeit, das Objekt von dieser Berechnung auszuschließen.
Die Antwort auf die dritte Frage lautet, dass in Bezug auf die ungeprüfte Funktionalität nichts verloren gehen würde (es würde keine unsichere Typumwandlung innerhalb der JUnit-API geben, wenn diese Methode nicht generisiert würde), aber sie versuchen, etwas anderes zu erreichen - stellen Sie statisch sicher, dass die Es ist wahrscheinlich, dass zwei Parameter übereinstimmen.
EDIT (nach weiterer Überlegung und Erfahrung):
Eines der großen Probleme bei der Signatur der assertThat-Methode ist der Versuch, eine Variable T mit einem generischen Parameter von T gleichzusetzen. Das funktioniert nicht, weil sie nicht kovariant sind. So können Sie beispielsweise ein T haben, das ein ist,
List<String>
aber dann eine Übereinstimmung übergeben, mit der der Compiler arbeitetMatcher<ArrayList<T>>
. Wenn es kein Typparameter wäre, wären die Dinge in Ordnung, da List und ArrayList kovariant sind, aber da Generics für den Compiler ArrayList erfordern, kann es eine Liste aus Gründen, von denen ich hoffe, dass sie klar sind, nicht tolerieren von Oben.quelle
List<Date>
von einer Methode mit Typ zurückgegebenList<Object>
? Das sollte sicher sein, auch wenn es Java nicht erlaubt.Es läuft darauf hinaus:
Sie können sehen, dass die Klassenreferenz c1 eine Long-Instanz enthalten könnte (da das zugrunde liegende Objekt zu einem bestimmten Zeitpunkt hätte sein können
List<Long>
), aber offensichtlich nicht in ein Datum umgewandelt werden kann, da nicht garantiert werden kann, dass die "unbekannte" Klasse Datum war. Es ist nicht typsicher, daher lässt der Compiler es nicht zu.Wenn wir jedoch ein anderes Objekt einführen, z. B. Liste (in Ihrem Beispiel ist dieses Objekt Matcher), wird Folgendes wahr:
... aber wenn der Typ der Liste wird? erweitert T anstelle von T ....
Ich denke, wenn Sie sich ändern
Matcher<T> to Matcher<? extends T>
, führen Sie im Grunde das Szenario ein, das der Zuweisung von l1 = l2 ähnelt.Es ist immer noch sehr verwirrend, verschachtelte Platzhalter zu haben, aber hoffentlich macht dies Sinn, warum es hilfreich ist, Generika zu verstehen, indem man sich ansieht, wie man generische Referenzen einander zuweisen kann. Es ist auch weiter verwirrend, da der Compiler den Typ von T ableitet, wenn Sie den Funktionsaufruf ausführen (Sie sagen nicht explizit, dass es T ist).
quelle
Der Grund, warum Ihr ursprünglicher Code nicht kompiliert wird,
<? extends Serializable>
ist nicht "jede Klasse, die Serializable erweitert", sondern "eine unbekannte, aber spezifische Klasse, die Serializable erweitert".Zum Beispiel gegeben , wie der Code geschrieben, ist es durchaus möglich , zuweisen
new TreeMap<String, Long.class>()>
zuexpected
. Wenn der Compiler das Kompilieren des Codes zulässt, wird derassertThat()
vermutlich unterbrochen, da erDate
Objekte anstelle derLong
in der Karte gefundenen Objekte erwartet .quelle
<Serializable>
Eine Möglichkeit für mich, Platzhalter zu verstehen, besteht darin, zu denken, dass der Platzhalter nicht den Typ der möglichen Objekte angibt, die eine bestimmte generische Referenz "haben" kann, sondern die Art anderer generischer Referenzen, mit denen er kompatibel ist (dies mag verwirrend klingen ...) Daher ist die erste Antwort in ihrem Wortlaut sehr irreführend.
Mit anderen Worten
List<? extends Serializable>
bedeutet, dass Sie diesen Verweis anderen Listen zuweisen können, in denen der Typ ein unbekannter Typ oder eine Unterklasse von Serializable ist. Denken Sie NICHT daran, dass A SINGLE LIST Unterklassen von Serializable enthalten kann (da dies eine falsche Semantik ist und zu einem Missverständnis von Generics führt).quelle
<? extends T>
kompiliert?Ich weiß, dass dies eine alte Frage ist, aber ich möchte ein Beispiel nennen, das meiner Meinung nach begrenzte Platzhalter ziemlich gut erklärt.
java.util.Collections
bietet diese Methode an:Wenn wir eine Liste von haben
T
, kann die Liste natürlich Instanzen von Typen enthalten, die sich erweiternT
. Wenn die Liste Tiere enthält, kann die Liste sowohl Hunde als auch Katzen (beide Tiere) enthalten. Hunde haben die Eigenschaft "woofVolume" und Katzen haben die Eigenschaft "meowVolume". Während wir nach diesen Eigenschaften sortieren möchten, die speziell für Unterklassen von geltenT
, wie können wir erwarten, dass diese Methode dies tut? Eine Einschränkung von Comparator besteht darin, dass nur zwei Dinge von nur einem Typ verglichen werden können (T
). Wenn Sie also einfach a benötigenComparator<T>
, ist diese Methode verwendbar. Der Schöpfer dieser Methode erkannte jedoch, dass wenn etwas ein istT
, es auch eine Instanz der Oberklassen von istT
. Daher erlaubt er uns, einen KomparatorT
oder eine beliebige Oberklasse von zu verwendenT
, dh? super T
.quelle
Was ist, wenn Sie verwenden
quelle