webentwicklung-frage-antwort-db.com.de

Wie erzwinge ich, dass ein generischer Typparameter eine Schnittstelle ist?

Gibt es eine Möglichkeit in Java zu spezifizieren, dass der Typparameter einer generischen Klasse eine Schnittstelle sein muss (nicht nur erweitern!)

Was ich tun möchte, ist folgendes:

public class MyClass<X extends SomeInterface, Y extends SomeOtherClass & X>

Das bedeutet, dass Y eine Unterklasse von SomeOtherClass AND UND X. sein muss. Was ich momentan vom Compiler bekomme, ist

Der Typ X ist keine Schnittstelle; Es kann nicht als begrenzter Parameter angegeben werden

Wie kann ich dem Compiler sagen, dass X immer eine Schnittstelle sein muss?

Bearbeiten:
OK, ich schätze, ich habe mein Problem etwas vereinfacht. Verwenden wir meine eigentliche Anwendungsdomäne, um sie klarer zu machen:

Ich habe eine API zur Darstellung von Diagrammen. Ein Diagramm enthält Knoten und Kante Objekte. Alle diese drei Klassen implementieren die Shape - Schnittstelle. Formen können untergeordnete Formen, eine übergeordnete Form und zu einem Diagramm gehören.

Die Sache ist, dass ich zwei Versionen dieser API erstellen muss: eine Open-Source-Version mit nur grundlegender Funktionalität und eine erweiterte mit mehr Funktionen. Die erweiterte API muss jedoch nur Methoden bereitstellen, die die erweiterten Typen zurückgeben ( ExtendedDiagram , ExtendedNode , ExtendedEdge und (hier kommt das Problem) ExtendedShape ).
Ich habe also so etwas:

/* BASIC CLASSES */
public interface Shape<X extends Shape<X,Y>, Y extends Diagram<X,Y>>{
    public List<X> getChildShapes();
    public X getParent();
    public Y getDiagram();
    ...
}

public class Diagram<X extends Shape<X,Y>, Y extends Diagram<X,Y>> implements Shape<X,Y>{...}
public class Edge<X extends Shape<X,Y>, Y extends Diagram<X,Y>> implements Shape<X,Y>{...}
...

/* EXTENDED CLASSES */
public interface ExtendedShape extends Shape<ExtendedShape,ExtendedDiagram> { ... }

public class ExtendedDiagram extends Diagram<ExtendedShape,ExtenedDiagram> implements ExtendedShape { ... }
public class ExtendedEdge extends Edge<ExtendedShape,ExtenedDiagram> implements ExtendedShape { ... }
...

Die erweiterte API funktioniert gut und der grundlegende API-Code gibt einige Warnungen aus. Das Hauptproblem tritt jedoch bei der Verwendung der Basis-API auf:

public class SomeImporter<X extends Shape<X,Y>, Y extends Diagram<X,Y>, E extends Edge<X,Y>>{
    private Y diagram;

    public void addNewEdge(E newEdge){
        diagram.addChildShape(newEdge);
    ...

Diese letzte Zeile gibt mir folgende Warnung:

Die Methode addChildShape (X) im Typ Diagram ist für die Argumente (E) nicht anwendbar.

Nun möchte ich nur noch angeben, dass E auch X implementieren muss, und alles wäre gut - hoffe ich;)

Ist das alles sinnvoll? Kennst du einen Weg, das zu tun? Oder gibt es eine bessere Möglichkeit, die erweiterte API mit den genannten Einschränkungen zu erhalten?
Vielen Dank, dass Sie bei mir bleiben, jede Hilfe wird sehr geschätzt!

42
Philipp Maschke

Sie können verwenden:

class Foo<T extends Number & Comparable> {...}

Eine Klasse Foo mit einem Typparameter, T. Foo, muss mit einem Typ instanziiert werden, der ein Subtyp von Number ist und der Comparable implementiert. 

13
idichekop

Im Kontext der Generics behandelt <Type extends IInterface> sowohl Erweiterungen als auch Implementierungen. Hier ist ein Beispiel:

public class GenericsTest<S extends Runnable> {
    public static void main(String[] args) {
        GenericsTest<GT> t = new GenericsTest<GT>();
        GenericsTest<GT2> t2 = new GenericsTest<GT>();
    }
}

class GT implements Runnable{
    public void run() {

    }
}

class GT2 {

}

GenericsTest akzeptiert GT, weil es Runnable implementiert. GT2 funktioniert nicht, daher schlägt es fehl, wenn versucht wird, die zweite GenericsTest-Instantiierung zu kompilieren.

3
Mike Thomsen

Vielleicht können Sie Ihr Modell ein wenig vereinfachen: Zu viele Generika werden in Bezug auf die Lesbarkeit schnell zu einem echten Schmerz, und dies ist ein Problem, wenn Sie eine öffentliche API definieren. Normalerweise, wenn Sie nicht mehr verstehen können, was in den Klammern sein sollte, dann gehen Sie zu weit für Ihre Bedürfnisse - und Sie können nicht erwarten, dass Benutzer dies besser verstehen als Sie selbst ...

Um den Code kompilieren zu können, können Sie versuchen, Folgendes zu definieren: Shape:

public <S extends Shape<?,?>> void addChildShape(S shape);

Das sollte es tun.

HTH

1
Vincent

Sie haben folgendes geschrieben:

public interface Shape<X extends Shape<X,Y>, Y extends Diagram<X,Y>>{
    public List<X> getChildShapes();
    public X getParent();
    public Y getDiagram();
    ...
}

Ich würde zumindest raten, die Variable vom Typ X wie folgt zu entfernen:

public interface Shape<Y>{
    public List<Shape<Y>> getChildShapes();
    public Shape<Y> getParent();
    public Diagram<Y> getDiagram();
    ...
}

Der Grund dafür ist, dass das, was Sie ursprünglich geschrieben haben, an einer möglicherweise unbeschränkten rekursiven Verschachtelung der Typparameter leidet. Eine Form kann in einer übergeordneten Form verschachtelt sein, die in einer anderen verschachtelt sein kann, die alle in der Typensignatur berücksichtigt werden muss ... kein gutes Rezept für die Lesbarkeit. In Ihrem Beispiel, in dem Sie "Shape <X>" anstelle von "Shape <Shape <X>" deklarieren, ist dies jedoch nicht der Fall, aber in diese Richtung gehen Sie, wenn Sie es jemals wollten Nutze eigentlich Shape alleine ...

Ich würde wahrscheinlich auch empfehlen, einen Schritt weiter zu gehen und die Y-Variable aus ähnlichen Gründen zu entfernen. Java-Generics sind mit dieser Art von Kompositionen nicht besonders gut zurechtkommen. Beim Versuch, statische Typen für diese Art der Modellierung mithilfe von Generics zu erzwingen, habe ich festgestellt, dass das Typsystem zusammenbricht, wenn Sie später mit der Erweiterung beginnen.

Es ist typisch für das Animal/Dog-Problem ... Animal hat ein getChildren (), aber die Kinder des Hundes müssen auch Hunde sein ... Java kann damit nicht gut umgehen, da (teilweise aufgrund fehlender abstrakter Typen wie in Sprachen) wie Scala, aber ich sage nicht, dass du Scala auch für dieses Problem verwenden solltest. Die Typvariablen müssen an allen möglichen Stellen deklariert werden, an denen sie nicht wirklich gehören.

1
nbryant

Ich bin vielleicht weit weg von hier, aber mein Verständnis von Generika ist etwas anders.

Ich bitte jemanden, der mich korrigiert, wenn ich falsch liege.

IMO -

Dies ist eine sehr verwirrende Struktur, die Sie haben. Sie haben unendlich viele Unterklassen von Shape, es sieht so aus.

Ihre Shape-Oberfläche wird auf dieselbe Weise wie eine HashMap verwendet, aber ich habe noch nie gesehen, wie eine HashMap das tut, was Sie versuchen. Schließlich muss X eine Klasse in Shape sein. Ansonsten machen Sie HashMap

Wenn Sie möchten, dass X immer eine "IS A" -Beziehung zu einer Schnittstelle ist, geschieht dies nicht. Dafür gibt es keine Generika. Generics werden verwendet, um Methoden auf mehrere Objekte anzuwenden, und Schnittstellen dürfen keine Objekte sein. Schnittstellen definieren einen Vertrag zwischen einem Kunden und einer Klasse. Mit the können Sie lediglich sagen, dass Sie jedes Objekt akzeptieren, das Runnable implementiert, da alle oder einige Ihrer Methoden erforderlich sind, um die Runnable-Schnittstellenmethoden zu verwenden. Andernfalls, wenn Sie nichts angeben und als definieren, kann Ihr Vertrag zwischen Ihrer Klasse mit dem Client unerwartetes Verhalten erzeugen und dazu führen, dass entweder der falsche Rückgabewert oder eine Ausnahme ausgelöst wird.

Zum Beispiel:

public interface Animal {
    void eat();

    void speak();
}

public interface Dog extends Animal {
    void scratch();

    void sniff();
}

public interface Cat extends Animal {
    void sleep();

    void stretch();
}

public GoldenRetriever implements Dog {
    public GoldenRetriever() { }

    void eat() {
        System.out.println("I like my Kibbles!");
    }

    void speak() {
        System.out.println("Rufff!");
    }

    void scratch() {
        System.out.println("I hate this collar.");
    }

    void sniff() {
        System.out.println("Ummmm?");
    }
}

public Tiger implements Cat {
    public Tiger() { }

    void eat() {
        System.out.println("This meat is tasty.");
    }

    void speak() {
        System.out.println("Roar!");
    }

    void sleep() {
        System.out.println("Yawn.");
    }

    void stretch() {
        System.out.println("Mmmmmm.");
    }
}

Wenn Sie diese Klasse gemacht haben, können Sie erwarten, dass Sie immer 'speak ()' & 'sniff ()' aufrufen können.

public class Kingdom<X extends Dog> {
    public Kingdom(X dog) {
        dog.toString();
        dog.speak();
        dog.sniff();
    }
}

Wenn Sie dies jedoch getan haben, KÖNNEN Sie IMMER NICHT 'speak ()' & 'sniff ()' anrufen.

public class Kingdom<X> {
    public Kingdom(X object) {
        object.toString();
        object.speak();
        object.sniff();
    }
}

FAZIT:

Generics geben Ihnen die Möglichkeit, Methoden für eine Vielzahl von Objekten zu verwenden, nicht für Schnittstellen. Ihr endgültiger Eintrag in ein generisches MUSS ein Objekttyp sein.

0
bdparrish

Das reservierte Wort wird zusammen mit einem Typparameter T zum Angeben einer Grenze verwendet.

"... in diesem Zusammenhang wird" extend "im Allgemeinen als" extend "(wie in Klassen) oder" implements "(wie in Interfaces) verstanden." [ https://docs.Oracle.com/ javase/tutorial/Java/generics/bounded.html ]

Kurz gesagt, mit "extend" kann nur eine Grenze (ob eine Klasse oder eine Schnittstelle) für einen Klassentypparameter T und nicht für einen Schnittstellentypparameter T angegeben werden.

public class MyClass <X erweitert SomeInterface, Y erweitert SomeOtherClass & X>

Der Compiler löst X als Klasse auf. Für das zweite Vorkommen von X zusammen mit dem Typparameter Y (der eindeutig ohnehin eine Klasse sein muss) muss X eine Schnittstelle sein. Da es X bereits als Klasse aufgelöst hat, signalisiert es den Fehler beim zweiten Auftreten von X,

Der Typ X ist keine Schnittstelle;

Wäre X beim ersten Auftreten als unbegrenzter Parameter angegeben worden, hätte der Compiler es entweder als Klasse oder als Schnittstelle aufgelöst und das zweite Vorkommen von X als mögliche Schnittstelle angesehen und damit die Kompilierung ermöglicht. Da dies nicht der Fall war, stellt der Compiler klar,

es kann nicht als begrenzter Parameter angegeben werden

0
rps

Verwenden Sie einen Vorprozessor, um die "reduzierte" Version Ihres Codes zu generieren. Die Verwendung von apt und Anmerkungen ist eine gute Möglichkeit, dies zu tun.

0
alex