web-dev-qa-db-fra.com

Comment forcer un paramètre de type générique à être une interface?

Existe-t-il un moyen en Java de spécifier que le paramètre type d'une classe générique doit être une interface (pas seulement pour l'étendre!)

Ce que je veux faire est le suivant:

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

Cela signifie que Y doit être une sous-classe de SomeOtherClass ET implémenter X . Ce que je reçois actuellement par le compilateur est

Le type X n'est pas une interface; il ne peut pas être spécifié en tant que paramètre lié

Alors, comment puis-je dire au compilateur que X doit toujours être une interface?

Modifier:
OK, je suppose que j'ai un peu simplifié mon problème. Utilisons mon domaine d'application actuel pour le rendre plus clair:

J'ai une API pour représenter les diagrammes. A Schéma contient Nœud et Bord objets. Ces trois classes implémentent l'interface Shape . Les formes peuvent avoir des formes enfants, une forme parent et appartenir à un diagramme.

Le fait est que je dois créer deux versions de cette API: une open-source avec uniquement des fonctionnalités de base et une version étendue avec plusieurs fonctionnalités. Cependant, l'API étendue ne doit fournir que des méthodes qui renvoient les types étendus ( ExtendedDiagram , ExtendedNode , ExtendedEdge et (voici le problème) ExtendedShape ).
J'ai donc quelque chose comme ça:

/* 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 { ... }
...

L'API étendue fonctionne correctement et le code de l'API de base donne des avertissements, mais le problème principal se produit lors de l'utilisation de l'API de base:

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

Cette dernière ligne me donne l'avertissement suivant:

La méthode addChildShape (X) dans le type Diagramme n'est pas applicable pour les arguments (E).

Alors maintenant, je voudrais juste préciser que E doit également mettre en œuvre X et que tout ira bien - j'espère;)

Est-ce que tout cela a du sens? Savez-vous un moyen de le faire? Ou existe-t-il même un meilleur moyen d'obtenir l'API étendue avec lesdites restrictions?
Merci de rester avec moi, toute aide est grandement appréciée!

42
Philipp Maschke

Vous pouvez utiliser:

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

Une classe Foo avec un paramètre de type, T. Foo doit être instanciée avec un type qui est un sous-type de Number et qui implémente Comparable. 

13
idichekop

Dans le contexte générique, <Type extends IInterface> gère à la fois les extensions et les implémentations. Voici un exemple:

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 acceptera GT car il implémente Runnable. GT2 ne le fait pas. Par conséquent, il échoue lors de la tentative de compilation de la seconde instanciation de GenericsTest.

3
Mike Thomsen

Peut-être pourriez-vous simplifier un peu votre modèle: trop de génériques deviennent rapidement un problème pour la lisibilité, et c'est tout un problème si vous définissez une API publique. Habituellement, si vous ne comprenez plus ce qui devrait être entre crochets, vous irez trop loin pour vos besoins - et vous ne pouvez pas vous attendre à ce que les utilisateurs le comprennent mieux que vous-même ...

Quoi qu'il en soit, pour que votre code soit compilé, vous pouvez essayer de définir quelque chose comme ceci, dans le type Shape:

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

Ça devrait le faire.

HTH

1
Vincent

Vous avez écrit ce qui suit:

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

Je conseillerais, au minimum, de supprimer la variable de type X, comme suit:

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

La raison en est que ce que vous avez écrit à l'origine souffre d'une imbrication récursive potentiellement illimitée des paramètres de type. Une forme peut être imbriquée dans une forme parent, qui peut être imbriquée dans une autre, le tout devant être pris en compte dans la signature de type ... ce n'est pas une bonne recette pour la lisibilité. Eh bien, cela ne se passe pas ainsi dans votre exemple, dans lequel vous déclarez "Forme <X>" au lieu de "Forme <Forme <X >>", mais c'est la direction dans laquelle vous vous dirigez, si jamais vous vouliez utilise réellement Shape seul ...

Je recommanderais probablement aussi d'aller un peu plus loin et d'éliminer la variable Y pour des raisons similaires. Les génériques Java ne s’adaptent pas très bien à ce type de composition. Lors de la tentative d'application de types statiques pour ce type de modélisation via des génériques, j'ai constaté que le système de types commençait à tomber en panne lorsque vous commenciez à étendre les choses ultérieurement.

C'est typique du problème Animal/Chien ... Animal a un getChildren (), mais les enfants de Dog doivent aussi être des chiens ... Java ne s'en occupe pas bien parce que (en partie à cause du manque de types abstraits comme dans les langages comme Scala, mais je ne dis pas que vous devriez vous précipiter et utiliser Scala pour résoudre ce problème non plus), les variables de type doivent commencer à être déclarées dans toutes sortes d'endroits où elles n'appartiennent pas vraiment.

1
nbryant

Je suis peut-être bien loin de la base ici, mais ma compréhension des médicaments génériques est un peu différente.

Je demande à quelqu'un de me corriger si je me trompe.

IMO -

C'est une structure très déroutante que vous avez. Vous avez des sous-classes de forme référencées à l'infini.

Votre interface Shape est utilisée de la même manière qu'un HashMap, mais je n’ai jamais vu un HashMap faire ce que vous essayez de faire, vous devez éventuellement avoir X une classe dans Shape. Sinon, vous faites HashMap

Si vous voulez toujours que X soit une relation "IS A" avec une interface, cela ne se produira pas. Ce n’est pas ce à quoi servent les génériques. Les génériques sont utilisés pour appliquer des méthodes à plusieurs objets et les interfaces ne peuvent pas être des objets. Les interfaces définissent un contrat entre un client et une classe. Tout ce que vous pouvez faire avec, c'est que vous accepterez n'importe quel objet implémentant Runnable, car toutes ou certaines de vos méthodes sont nécessaires pour utiliser les méthodes d'interface Runnable. Sinon, si vous ne spécifiez pas et définissez comme, votre contrat entre la classe et le client peut alors produire un comportement inattendu et provoquer le déclenchement d'une valeur de retour incorrecte ou une exception.

Par exemple:

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

Maintenant, si vous avez fait cette classe, vous pouvez toujours vous attendre à appeler 'speak ()' & 'sniff ()'

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

Cependant, si vous faites cela, vous NE POUVEZ PAS TOUJOURS appeler 'speak ()' & 'sniff ()'

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

CONCLUSION:

Les génériques vous permettent d'utiliser des méthodes sur une large gamme d'objets, et non d'interfaces. Votre dernière entrée dans un générique DOIT être un type d’objet.

0
bdparrish

Utilisez un pré-processeur pour générer la version "réduite" de votre code. Utiliser apt et des annotations peut être un bon moyen de le faire.

0
alex