Wann benötigen Java-Generika <? erweitert T> anstelle von <T> und gibt es einen Nachteil beim Umschalten?

205

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 assertThatSignatur 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 assertThatMethodensignatur in Folgendes ändere :

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

Dann funktioniert die Zusammenstellung.

Also drei Fragen:

  1. 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.
  2. Gibt es einen Nachteil bei der Änderung der assertThatMethode auf Matcher<? extends T>? Gibt es andere Fälle, die brechen würden, wenn Sie das tun würden?
  3. Hat die Generierung der assertThatMethode in JUnit irgendeinen Sinn ? Die MatcherKlasse 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());
    }
}
Yishai
quelle
Dieser Link ist sehr nützlich (Generika, Vererbung und Subtyp): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Antworten:

145

Erstens - ich muss Sie an http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html weiterleiten - sie macht einen tollen Job.

Die Grundidee ist, dass Sie verwenden

<T extends SomeClass>

wenn der tatsächliche Parameter ein SomeClassbeliebiger Subtyp sein kann.

In Ihrem Beispiel

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

Sie sagen, das expectedkann Klassenobjekte enthalten, die jede Klasse darstellen, die implementiert wird Serializable. Ihre Ergebniskarte sagt, dass es nur halten kannDate Klassenobjekte enthalten kann.

Wenn Sie in Folge passieren, du bist Einstellung Tgenau Mapder Stringauf DateKlassenobjekte, die nicht überein Mapder , Stringdass die zu nichts Serializable.

Eine Sache zu überprüfen - sind Sie sicher, dass Sie wollen Class<Date>und nicht Date? Eine Karte von Stringto Class<Date>klingt im Allgemeinen nicht besonders nützlich (alles, was sie enthalten kann, sind Date.classWerte und keine Instanzen vonDate ).

Bei der Generierung assertThatbesteht die Idee darin, dass die Methode sicherstellen kann, dass ein Matcherzum Ergebnistyp passender Wert übergeben wird.

Scott Stanchfield
quelle
In diesem Fall möchte ich ja eine Karte der Klassen. In dem Beispiel, das ich gegeben habe, wurden Standard-JDK-Klassen anstelle meiner benutzerdefinierten Klassen verwendet. In diesem Fall wird die Klasse jedoch tatsächlich durch Reflektion instanziiert und basierend auf dem Schlüssel verwendet. (Eine verteilte App, in der dem Client nicht die Serverklassen zur Verfügung stehen, sondern nur der Schlüssel der Klasse, die für die serverseitige Arbeit verwendet werden soll.)
Yishai
6
Ich denke, mein Gehirn steckt fest, warum eine Karte mit Klassen vom Typ Datum nicht einfach gut in eine Art Karten passt, die Klassen vom Typ Serializable enthält. Sicher, die Klassen vom Typ Serializable können auch andere Klassen sein, aber sie enthalten sicherlich den Typ Date.
Yishai
Auf der assertDas, um sicherzustellen, dass der Cast für Sie ausgeführt wird, ist es der matcher.matches () -Methode egal. Da das T also nie verwendet wird, warum sollte es dann verwendet werden? (Der Rückgabetyp der Methode ist ungültig)
Yishai
Ahhh - das ist es, was ich bekomme, wenn ich den Def der Behauptung nicht gelesen habe. Sieht so aus, als ob nur sichergestellt werden soll, dass ein passender Matcher übergeben wird ...
Scott Stanchfield
28

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:

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

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:

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

Möchte beides können:

T x = list.get(0);

und

list.add(value);

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:

Matcher<? extends T>

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 arbeitet Matcher<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.

Yishai
quelle
Ich verstehe immer noch nicht, warum ich nicht upcast kann. Warum kann ich eine Liste von Daten nicht in eine Liste von serialisierbaren Daten umwandeln?
Thomas Ahle
@ThomasAhle, da Referenzen, die glauben, dass es sich um eine Liste von Daten handelt, auf Casting-Fehler stoßen, wenn sie Strings oder andere serialisierbare Elemente finden.
Yishai
Ich verstehe, aber was ist, wenn ich die alte Referenz irgendwie losgeworden bin, als hätte ich die List<Date>von einer Methode mit Typ zurückgegeben List<Object>? Das sollte sicher sein, auch wenn es Java nicht erlaubt.
Thomas Ahle
14

Es läuft darauf hinaus:

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

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:

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

... aber wenn der Typ der Liste wird? erweitert T anstelle von T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

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

GreenieMeanie
quelle
9

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>()>zu expected. Wenn der Compiler das Kompilieren des Codes zulässt, wird der assertThat()vermutlich unterbrochen, da er DateObjekte anstelle der Longin der Karte gefundenen Objekte erwartet .

erickson
quelle
1
Ich folge nicht ganz - wenn Sie sagen "bedeutet nicht ... aber ...": Was ist der Unterschied? (Wie wäre es mit einem Beispiel für eine "bekannte, aber unspezifische" Klasse, die der ersteren Definition entspricht, aber nicht der letzteren?)
Poundifdef
Ja, das ist etwas umständlich. Ich bin mir nicht sicher, wie ich es besser ausdrücken soll. Ist es sinnvoller zu sagen: "'?' ist ein Typ, der unbekannt ist, kein Typ, der zu irgendetwas passt? "
Erickson
1
Was helfen kann, um es zu erklären, ist, dass für "jede Klasse, die Serializable erweitert" Sie einfach verwenden könnten<Serializable>
c0der
8

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

GreenieMeanie
quelle
Das hilft sicherlich, aber das "klingt vielleicht verwirrend" wird durch ein "klingt verwirrend" ersetzt. Warum wird die Methode mit Matcher nach dieser Erklärung <? extends T>kompiliert?
Yishai
Wenn wir List <Serializable> definieren, macht es dasselbe, oder? Ich spreche von Ihrem 2. Absatz. dh Polymorphismus würde damit umgehen?
Supun Wijerathne
3

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.Collectionsbietet diese Methode an:

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

Wenn wir eine Liste von haben T, kann die Liste natürlich Instanzen von Typen enthalten, die sich erweitern T. 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 gelten T, 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ötigen Comparator<T>, ist diese Methode verwendbar. Der Schöpfer dieser Methode erkannte jedoch, dass wenn etwas ein ist T, es auch eine Instanz der Oberklassen von ist T. Daher erlaubt er uns, einen Komparator Toder eine beliebige Oberklasse von zu verwenden T, dh ? super T.

Lucas Ross
quelle
1

Was ist, wenn Sie verwenden

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
quelle
Ja, das ist eine Art, worauf meine obige Antwort abzielte.
GreenieMeanie
Nein, das hilft der Situation nicht weiter, zumindest so, wie ich es versucht habe.
Yishai