Obergrenze des generischen Rückgabetyps - Schnittstelle vs. Klasse - überraschend gültiger Code

171

Dies ist ein reales Beispiel aus einer Bibliotheks-API eines Drittanbieters, jedoch vereinfacht.

Kompiliert mit Oracle JDK 8u72

Betrachten Sie diese beiden Methoden:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Beide melden eine Warnung "ungeprüfte Besetzung" - ich verstehe warum. Was mich verblüfft, ist, warum ich anrufen kann

Integer x = getCharSequence();

und es kompiliert? Der Compiler sollte wissen, dass Integerdies nicht implementiert wird CharSequence. Der Anruf an

Integer y = getString();

gibt einen Fehler (wie erwartet)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Kann jemand erklären, warum dieses Verhalten als gültig angesehen wird? Wie wäre es nützlich?

Der Client weiß nicht, dass dieser Aufruf unsicher ist - der Code des Clients wird ohne Warnung kompiliert. Warum warnt die Kompilierung nicht davor / gibt einen Fehler aus?

Wie unterscheidet es sich von diesem Beispiel:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Der Versuch, zu bestehen List<Integer>, führt erwartungsgemäß zu einem Fehler:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Wenn dies als Fehler gemeldet wird, warum Integer x = getCharSequence();nicht?

Adam Michalik
quelle
15
interessant! Casting auf der LHS Integer x = getCharSequence();wird kompiliert, aber Casting auf der RHS Integer x = (Integer) getCharSequence();schlägt fehl kompilieren
Flocken
Welche Version des Java-Compilers verwenden Sie? Bitte geben Sie diese Informationen in der Frage an.
Federico Peralta Schaffner
@FedericoPeraltaSchaffner kann nicht verstehen, warum das wichtig ist - dies ist eine Frage direkt zum JLS.
Boris die Spinne
@ BoristheSpider Weil sich der Typinferenzmechanismus für Java8 geändert hat
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner - Ich habe die Frage bereits mit [java-8] markiert, aber jetzt habe ich die Compiler-Version in den Beitrag aufgenommen.
Adam Michalik

Antworten:

184

CharSequenceist ein interface. Selbst wenn SomeClassdies nicht implementiert wird, ist CharSequencees daher durchaus möglich, eine Klasse zu erstellen

class SubClass extends SomeClass implements CharSequence

Deshalb kannst du schreiben

SomeClass c = getCharSequence();

weil der abgeleitete Typ Xder Schnittpunkttyp ist SomeClass & CharSequence.

Dies ist etwas seltsam im Fall von Integerweil Integerist endgültig, spielt aber finalin diesen Regeln keine Rolle. Zum Beispiel können Sie schreiben

<T extends Integer & CharSequence>

Auf der anderen Seite Stringist keine interface, so wäre es unmöglich zu erweitern SomeClass, um einen Subtyp von zu erhalten String, da Java keine Mehrfachvererbung für Klassen unterstützt.

Bei diesem ListBeispiel müssen Sie sich daran erinnern, dass Generika weder kovariant noch kontravariant sind. Dies bedeutet , dass wenn Xein Subtyp ist Y, List<X>ist weder ein Subtyp noch ein übergeordneter Typ von List<Y>. Da Integernicht implementiert CharSequence, können Sie nicht List<Integer>in Ihrer doCharSequenceMethode verwenden.

Sie können dies jedoch zum Kompilieren bringen

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Wenn Sie eine Methode , die zurückgibt ein List<T>wie folgt aus :

static <T extends CharSequence> List<T> foo() 

du kannst tun

List<? extends Integer> list = foo();

Dies liegt wiederum daran, dass der abgeleitete Typ ist Integer & CharSequenceund dies ein Subtyp von ist Integer.

Schnittpunkttypen treten implizit auf, wenn Sie mehrere Grenzen angeben (z <T extends SomeClass & CharSequence>. B. ).

Weitere Informationen finden Sie in dem Teil des JLS, in dem erläutert wird, wie Typgrenzen funktionieren. Sie können mehrere Schnittstellen einschließen, z

<T extends String & CharSequence & List & Comparator>

aber nur die erste Grenze darf eine Nicht-Schnittstelle sein.

Paul Boddington
quelle
62
Ich hatte keine Ahnung, dass Sie eine &in die generische Definition einfügen könnten . +1
Flocken
13
@flkes Sie können mehrere einfügen, aber nur das erste Argument kann eine Nicht-Schnittstelle sein. <T extends String & List & Comparator>ist ok aber <T extends String & Integer>nicht, weil Integeres keine Schnittstelle ist.
Paul Boddington
7
@PaulBoddington Es gibt einige praktische Anwendungen für diese Methoden. Zum Beispiel, wenn der Typ nicht tatsächlich für gespeicherte Daten verwendet wird. Beispiele hierfür sind Collections.emptyList()ebenso wie Optional.empty(). Diese geben Implementierungen einer generischen Schnittstelle zurück, speichern jedoch nichts.
Stefan Dollase
6
Und niemand sagt, dass eine Klasse zur finalKompilierungszeit finalzur Laufzeit sein wird.
Holger
7
@Federico Peralta Schaffner: Der Punkt hier ist, dass die Methode getCharSequence()verspricht, alles zurückzugeben, was Xder Anrufer benötigt, einschließlich der Rückgabe eines Typs, der erweitert Integerund implementiert wird, CharSequencewenn der Anrufer ihn benötigt, und unter diesem Versprechen ist es richtig, das Zuweisen des Ergebnisses zuzulassen Integer. Es ist die Methode, getCharSequence()die kaputt ist, weil sie ihr Versprechen nicht hält, aber das ist nicht die Schuld des Compilers.
Holger
59

Der Typ, der von Ihrem Compiler vor der Zuweisung für abgeleitet wird, Xist Integer & CharSequence. Dieser Typ fühlt sich komisch an, weil er Integerendgültig ist, aber es ist ein vollkommen gültiger Typ in Java. Es wird dann gegossen Integer, was vollkommen in Ordnung ist.

Es gibt genau einen möglichen Wert für den Integer & CharSequenceTyp : null. Mit folgender Implementierung:

<X extends CharSequence> X getCharSequence() {
    return null;
}

Die folgende Aufgabe funktioniert:

Integer x = getCharSequence();

Aufgrund dieses möglichen Wertes gibt es keinen Grund, warum die Zuordnung falsch sein sollte, auch wenn sie offensichtlich nutzlos ist. Eine Warnung wäre nützlich.

Das eigentliche Problem ist die API, nicht die Aufrufseite

Tatsächlich habe ich kürzlich über dieses Anti-Pattern für API-Designs gebloggt . Sie sollten (fast) niemals eine generische Methode entwerfen, um beliebige Typen zurückzugeben, da Sie (fast) niemals garantieren können, dass der abgeleitete Typ geliefert wird. Eine Ausnahme bilden Methoden wie die Collections.emptyList(), bei denen die Leere der Liste (und das Löschen des generischen Typs) der Grund ist, warum eine Schlussfolgerung für <T>funktioniert:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
quelle