Warum können Sie in einem begrenzten Platzhalter-Generikum nicht mehrere Schnittstellen haben?

74

Ich weiß, dass es alle möglichen kontraintuitiven Eigenschaften der generischen Typen von Java gibt. Hier ist eine, die ich nicht verstehe und von der ich hoffe, dass sie mir jemand erklären kann. Wenn Sie einen Typparameter für eine Klasse oder Schnittstelle angeben, können Sie ihn so binden, dass mehrere Schnittstellen mit implementiert werden müssen public class Foo<T extends InterfaceA & InterfaceB>. Wenn Sie jedoch ein tatsächliches Objekt instanziieren, funktioniert dies nicht mehr. List<? extends InterfaceA>ist in Ordnung, kann aber List<? extends InterfaceA & InterfaceB>nicht kompiliert werden. Betrachten Sie das folgende vollständige Snippet:

import java.util.List;

public class Test {

  static interface A {
    public int getSomething();
  }

  static interface B {
    public int getSomethingElse();
  }

  static class AandB implements A, B {
    public int getSomething() { return 1; }
    public int getSomethingElse() { return 2; }
  }

  // Notice the multiple bounds here. This works.
  static class AandBList<T extends A & B> {
    List<T> list;

    public List<T> getList() { return list; }
  }

  public static void main(String [] args) {
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    // This last one fails to compile!
    List<? extends A & B> foobar = new LinkedList<AandB>();
  }
}

Es scheint, dass die Semantik von bargenau definiert sein sollte - ich kann mir keinen Verlust an Typensicherheit vorstellen, indem ich eine Schnittmenge von zwei Typen anstelle von nur einem zulasse. Ich bin mir sicher, dass es eine Erklärung gibt. Weiß jemand was es ist?

Adrian Petrescu
quelle
Zwei Punkte: Erstens würde es besser aussehen wie "erweitert A, B". Zweitens, dass A und B keine Schnittstelle, sondern eine Klasse sein können (dies könnte natürlich vom Compiler erkannt werden).
SJuan76
8
Nee. Das &Zeichen ist die Standardmethode, um mehrere Grenzen zu kennzeichnen. " class AandBList<T extends A & B>" ist genau so, wie die Sprache funktioniert, obwohl ich damit einverstanden bin, dass es intuitiver wäre, sie zu verwenden, <T extends A, B>um sie abzugleichen public interface A extends C, D. Und in generischen Typgrenzen verwenden Sie extendsunabhängig davon, ob es sich um eine Schnittstelle oder eine Klasse handelt. Verwirrend, ich weiß, aber so ist es derzeit.
Adrian Petrescu

Antworten:

41

Interessanterweise java.lang.reflect.WildcardTypesieht die Schnittstelle so aus, als würde sie sowohl obere als auch untere Grenzen für ein Platzhalterargument unterstützen. und jede kann mehrere Grenzen enthalten

Type[] getUpperBounds();
Type[] getLowerBounds();

Dies geht weit über das hinaus, was die Sprache zulässt. Es gibt einen versteckten Kommentar im Quellcode

// one or many? Up to language spec; currently only one, but this API
// allows for generalization.

Der Autor der Schnittstelle scheint der Ansicht zu sein, dass dies eine zufällige Einschränkung ist.

Die vorgefertigte Antwort auf Ihre Frage lautet: Generika sind bereits zu kompliziert. Das Hinzufügen von mehr Komplexität könnte sich als letzter Strohhalm erweisen.

Damit ein Platzhalter mehrere Obergrenzen haben kann, muss die Spezifikation gescannt und sichergestellt werden, dass das gesamte System weiterhin funktioniert.

Ein Problem, das ich kenne, wäre die Typinferenz. Die aktuellen Inferenzregeln können einfach nicht mit Schnittpunkttypen umgehen. Es gibt keine Regel, um eine Einschränkung zu reduzieren A&B << C. Wenn wir es auf reduzieren

    A<<C 
  or
    A<<B

Jede Strominferenzmaschine muss einer umfassenden Überholung unterzogen werden, um eine solche Gabelung zu ermöglichen. Das eigentliche ernsthafte Problem ist jedoch, dass dies mehrere Lösungen ermöglicht, aber es gibt keine Rechtfertigung, einander vorzuziehen.

Inferenz ist jedoch für die Typensicherheit nicht wesentlich; In diesem Fall können wir einfach die Schlussfolgerung ablehnen und den Programmierer bitten, Typargumente explizit einzugeben. Daher ist die Schwierigkeit der Inferenz kein starkes Argument gegen Intercection-Typen.

unwiderlegbar
quelle
2
Sehr nachdenkliche Antwort. Und ein ziemlich interessanter Fund über java.lang.reflect.WildcardType. Vielen Dank :)
Adrian Petrescu
Ich verstehe diese Antwort wirklich nicht
gstackoverflow
Meinen Sie " Kreuzungstypen "?
Noyo
Warum ist es dann in Ordnung, wenn Typargumente mehrere Grenzen haben?
Dean Leitersdorf
25

Aus der Java-Sprachspezifikation :

4.9 Schnittpunkttypen Ein Schnittpunkttyp hat die Form T1 & ... & Tn, n> 0, wobei Ti, 1in Typausdrücke sind. Schnittpunkttypen entstehen bei den Prozessen der Capture-Konvertierung (§5.1.10) und der Typinferenz (§15.12.2.7). Es ist nicht möglich, einen Schnittpunkttyp direkt als Teil eines Programms zu schreiben. Keine Syntax unterstützt dies . Die Werte eines Schnittpunkttyps sind diejenigen Objekte, die Werte aller Typen Ti für 1 Zoll sind.

Warum wird dies nicht unterstützt? Ich denke, was solltest du mit so etwas machen? - Nehmen wir an, es wäre möglich:

List<? extends A & B> list = ...

Was soll es dann sein?

list.get(0);

Rückkehr? Es gibt keine Syntax, um einen Rückgabewert von zu erfassen A & B. Das Hinzufügen von etwas zu einer solchen Liste wäre ebenfalls nicht möglich, daher ist es im Grunde genommen nutzlos.

prägen
quelle
3
Danke für den Link! Das ist wirklich hilfreich. Ich denke, was ich damit machen würde, ist das A a = list.get(0);und B b = list.get(0);beide wären sicher. Ich denke, ich kann immer umgestalten, um eine gemeinsame Superschnittstelle zu erhalten.
Adrian Petrescu
16
>>> Es gibt keine Syntax, um einen Rückgabewert von A & B zu erfassen. Das Hinzufügen von etwas zu einer solchen Liste wäre ebenfalls nicht möglich, daher ist es im Grunde genommen nutzlos. Wenn Sie extenddiese Liste sowieso nicht einfügen können. und um zu bekommen, kann man einfach sagen A a = list.get(0)oder B b = list.get(0). Ich glaube nicht, dass dies ein typentheoretisches Problem ist, es ist nur eine Java-Einschränkung. Auf der anderen Seite <? Super A & B> ist fast bedeutungslos.
Op De Cirkel
Einverstanden mit @OpDeCirkel. Dies tritt bereits auf, wenn Sie einen Typ haben, SomeClass<T extends A & B>der eine zurückgebende Methode deklariert List<T>, und diese Methode dann für eine Variable vom Typ aufrufen SomeClass<?>. Also +1 für die JLS-Referenz, aber -1 für die Vermutung.
Paul Bellora
1
Angenommen, A und B sind Schnittstellen, und alle von J, K, L implementieren sowohl A als auch B (unter anderen Schnittstellen, die sie implementieren). Dann listkönnten Elemente von J, K und L enthalten und list.get(0)sie zurückgeben. In der Java-Spezifikation wird tatsächlich korrekt darauf hingewiesen, dass es keine Möglichkeit gibt, dieses Konzept direkt auszudrücken, aber J, K und L verwenden es indirekt durch mehrfache Vererbung von Schnittstellen.
Achow
-1 Wie Bohemian betont, ist dies mit einer etwas anderen Syntax möglich.
Aleksandr Dubinsky
11

Kein Problem ... deklarieren Sie einfach den Typ, den Sie in der Methodensignatur benötigen.

Dies kompiliert:

public static <T extends A & B> void main(String[] args) throws Exception
{
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    List<T> foobar = new LinkedList<T>(); // This compiles!
}
Böhmisch
quelle
Das ist wahr, aber es erklärt nicht , warum der Wert für den Typ Argument nicht anders sein kann , als Referenztyp wie definiert JSL ist Referenztyp definiert hier
Op De Cirkel
Entschuldigung, ich meinte: WildcardBounds können nicht anders sein alsextends ReferenceType | super ReferenceType
Op De Cirkel
1

Gute Frage. Ich brauchte eine Weile, um es herauszufinden.

Vereinfachen wir Ihren Fall: Sie versuchen dasselbe zu tun, als ob Sie eine Klasse deklarieren, die zwei Schnittstellen erweitert, und dann eine Variable, die diese beiden Schnittstellen als Typ hat, ungefähr so:

  class MyClass implements Int1, Int2 { }

  Int1 & Int2 variable = new MyClass()

Natürlich illegal. Und das entspricht dem, was Sie mit Generika versuchen. Was Sie versuchen zu tun ist:

  List<? extends A & B> foobar;

Um foobar zu verwenden, müssten Sie jedoch eine Variable beider Schnittstellen folgendermaßen verwenden :

  A & B element = foobar.get(0);

Was in Java nicht legal ist . Dies bedeutet, dass Sie die Elemente der Liste gleichzeitig als zwei Typen deklarieren, und selbst wenn unser Gehirn damit umgehen kann, kann die Java-Sprache dies nicht.

Mr.Eddart
quelle
11
Es stimmt, aber indem wir beide Typen gleichzeitig sind, können wir es sicher als den einen oder anderen behandeln, unabhängig zu unterschiedlichen Zeiten. Beides A a = list.get(0);und B b = list.get(0);wäre gültig.
Adrian Petrescu
"müsste brauchen" ist falsch, Sie sind nicht dazu gezwungen, ich sehe keine Einschränkung, es ist auch legal, dies zu tun: class C<T extends A & B> { public T foo() {};}wo Sie eine Variable mit mehreren Grenzen zurückgeben.
WorldSEnder
Das Problem besteht darin, das Ergebnis direkt zu verwenden und nicht in einer Variablen zu speichern. Für welche Methoden stehen beispielsweise Daten zur Verfügung list.get(0).*?
ndm13
-1

Für das, was es wert ist: Wenn sich jemand darüber wundert, weil er dies wirklich gerne in der Praxis anwenden möchte, habe ich es umgangen, indem ich eine Schnittstelle definiert habe, die die Vereinigung aller Methoden in allen Schnittstellen und Klassen enthält, mit denen ich arbeite. dh ich habe versucht, folgendes zu tun:

class A {}

interface B {}

List<? extends A & B> list;

was illegal ist - also habe ich stattdessen folgendes gemacht:

class A {
  <A methods>
}

interface B {
  <B methods>
}

interface C {
  <A methods>
  <B methods>
}

List<C> list;

Dies ist immer noch nicht so nützlich wie List<? extends A implements B>das Eingeben von etwas, wie wenn beispielsweise jemand Methoden zu A oder B hinzufügt oder entfernt, die Eingabe der Liste nicht automatisch aktualisiert wird, sondern eine manuelle Änderung von C erforderlich ist. Aber es funktioniert meine Bedürfnisse.

Kennzeichen
quelle
Wenn Sie dies tun, warum erweitert Klasse C nicht A implementiert B? Sicher, Sie müssen C manuell aktualisieren, wenn neue Methoden in A oder B eingeführt werden, aber zumindest sind Sie sicher, dass niemand vergessen wird, C zu aktualisieren, was in Ihrem Fall absolut möglich ist. (Sie werden es nicht vergessen, da NoClassDefFound einen Kompilierungszeitfehler aufweist.)
Keey
@Keey ich die Details meines Anwendungsfalles nicht mehr erinnern kann , aber es ist möglich , ich brauchte C eine Schnittstelle zu sein , weil ich es zu einer Klasse befestigen benötigt , die bereits gezwungen waren , etwas anderes als A. zu verlängern
Mark