Comment créer une classe Java qui implémente une interface avec deux types génériques?

164

J'ai une interface générique

public interface Consumer<E> {
    public void consume(E e);
}

J'ai une classe qui consomme deux types d'objets, donc j'aimerais faire quelque chose comme:

public class TwoTypesConsumer implements Consumer<Tomato>, Consumer<Apple>
{
   public void consume(Tomato t) {  .....  }
   public void consume(Apple a) { ...... }
}

Apparemment, je ne peux pas faire ça.

Je peux bien sûr mettre en œuvre l'envoi moi-même,

public class TwoTypesConsumer implements Consumer<Object> {
   public void consume(Object o) {
      if (o instanceof Tomato) { ..... }
      else if (o instanceof Apple) { ..... }
      else { throw new IllegalArgumentException(...) }
   }
}

Mais je recherche la solution de vérification de type et de répartition au moment de la compilation fournie par les génériques.

La meilleure solution à laquelle je puisse penser est de définir des interfaces séparées, par exemple

public interface AppleConsumer {
   public void consume(Apple a);
}

Fonctionnellement, cette solution est OK, je pense. C'est juste verbeux et moche.

Des idées?

daphshez
la source
Pourquoi avez-vous besoin de deux interfaces génériques du même type de base?
akarnokd
6
En raison de l'effacement de type, vous ne pouvez pas faire cela. Gardez-le deux classes différentes qui implémentent consumer. Crée plus de petites classes mais garde votre code générique (n'utilisez pas la réponse acceptée, cela brise tout le concept ... vous ne pouvez pas traiter le TwoTypesConsumer comme un consommateur, ce qui est MAUVAIS).
Lewis Diamond
Vérifiez ceci pour le style fonctionnel impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Réponses:

78

Pensez à l'encapsulation:

public class TwoTypesConsumer {
    private TomatoConsumer tomatoConsumer = new TomatoConsumer();
    private AppleConsumer appleConsumer = new AppleConsumer();

    public void consume(Tomato t) { 
        tomatoConsumer.consume(t);
    }

    public void consume(Apple a) { 
        appleConsumer.consume(a);
    }

    public static class TomatoConsumer implements Consumer<Tomato> {
        public void consume(Tomato t) {  .....  }
    }

    public static class AppleConsumer implements Consumer<Apple> {
        public void consume(Apple a) {  .....  }
    }
}

Si la création de ces classes internes statiques vous dérange, vous pouvez utiliser des classes anonymes:

public class TwoTypesConsumer {
    private Consumer<Tomato> tomatoConsumer = new Consumer<Tomato>() {
        public void consume(Tomato t) {
        }
    };

    private Consumer<Apple> appleConsumer = new Consumer<Apple>() {
        public void consume(Apple a) {
        }
    };

    public void consume(Tomato t) {
        tomatoConsumer.consume(t);
    }

    public void consume(Apple a) {
        appleConsumer.consume(a);
    }
}
Steve McLeod
la source
2
en quelque sorte, cela ressemble à une duplication de code ... J'ai rencontré le même problème et je n'ai trouvé aucune autre solution qui semble propre.
bln-tom le
109
Mais neTwoTypesConsumer remplit aucun contrat, alors à quoi ça sert? Il ne peut pas être transmis à une méthode qui souhaite l'un ou l'autre type de Consumer. L'idée même d'un consommateur à deux types serait que vous pouvez le donner à une méthode qui veut un consommateur de tomate ainsi qu'à une méthode qui veut un consommateur de pomme. Ici, nous n'avons ni l'un ni l'autre.
Jeff Axelrod
@JeffAxelrod Je rendrais les classes internes non statiques afin qu'elles aient accès à l' TwoTypesConsumerinstance englobante si nécessaire, puis vous pouvez passer twoTypesConsumer.getAppleConsumer()à une méthode qui souhaite un consommateur Apple. Une autre option serait d'ajouter des méthodes similaires addConsumer(Producer<Apple> producer)à TwoTypesConsumer.
herman le
Cela ne fonctionne pas si vous n'avez pas le contrôle de l'interface (par exemple cxf / rs ExceptionMapper) ...
vikingsteve
17
Je vais le dire: c'est une faille avec Java. Il n'y a absolument aucune raison pour laquelle nous ne devrions pas être autorisés à avoir plusieurs implémentations de la même interface, à condition que les implémentations prennent des arguments différents.
gromit190
41

En raison de l'effacement de type, vous ne pouvez pas implémenter la même interface deux fois (avec des paramètres de type différents).

Shimi Bandiel
la source
6
Je peux voir comment c'est un problème ... la question est alors de savoir quel est le meilleur moyen (le plus efficace, le plus sûr, le plus élégant) de contourner ce problème.
daphshez
2
Sans entrer dans la logique métier, quelque chose ici «sent» le modèle Visiteur.
Shimi Bandiel
12

Voici une solution possible basée sur celle de Steve McLeod :

public class TwoTypesConsumer {
    public void consumeTomato(Tomato t) {...}
    public void consumeApple(Apple a) {...}

    public Consumer<Tomato> getTomatoConsumer() {
        return new Consumer<Tomato>() {
            public void consume(Tomato t) {
                consumeTomato(t);
            }
        }
    }

    public Consumer<Apple> getAppleConsumer() {
        return new Consumer<Apple>() {
            public void consume(Apple a) {
                consumeApple(t);
            }
        }
    }
}

L'exigence implicite de la question était Consumer<Tomato>et les Consumer<Apple>objets qui partagent l'état. Le besoin d' Consumer<Tomato>, Consumer<Apple>objets provient d'autres méthodes qui les attendent comme paramètres. J'ai besoin d'une classe pour les implémenter tous les deux afin de partager l'état.

L'idée de Steve était d'utiliser deux classes internes, chacune implémentant un type générique différent.

Cette version ajoute des getters pour les objets qui implémentent l'interface Consumer, qui peuvent ensuite être passés à d'autres méthodes qui les attendent.

daphshez
la source
2
Si quelqu'un utilise ceci: cela vaut la peine de stocker les Consumer<*>instances dans les champs d'instance si elle get*Consumerest appelée souvent.
TWiStErRob
7

Au moins, vous pouvez apporter une petite amélioration à votre implémentation de la répartition en procédant comme suit:

public class TwoTypesConsumer implements Consumer<Fruit> {

Fruit étant un ancêtre de la tomate et de la pomme.

Buhb
la source
14
Merci, mais quoi qu'en disent les pros, je ne considère pas la tomate comme un fruit. Malheureusement, il n'y a pas de classe de base commune autre que Object.
daphshez
2
Vous pouvez toujours créer une classe de base appelée: AppleOrTomato;)
Shimi Bandiel
1
Mieux, ajoutez un fruit qui délègue à Apple ou à la tomate.
Tom Hawtin - Tacle du
@Tom: À moins que je ne comprenne mal ce que vous dites, votre suggestion ne fait que faire avancer le problème, car pour que Fruit puisse déléguer à Apple ou à Tomato, Fruit doit avoir un champ d'une superclasse à la fois Apple et Tomato se référant à l'objet auquel il délègue.
Buhb
1
Cela impliquerait que TwoTypesConsumer peut consommer n'importe quel type de Fruit, tout actuellement implémenté et n'importe qui peut implémenter dans le futur.
Tom Gillen
3

Je suis juste tombé dessus. C'est juste arrivé, que j'ai eu le même problème, mais je l'ai résolu d'une manière différente: je viens de créer une nouvelle interface comme celle-ci

public interface TwoTypesConsumer<A,B> extends Consumer<A>{
    public void consume(B b);
}

malheureusement, ceci est considéré comme Consumer<A>et PAS comme Consumer<B>contre tout Logic. Vous devez donc créer un petit adaptateur pour le deuxième consommateur comme celui-ci dans votre classe

public class ConsumeHandler implements TwoTypeConsumer<A,B>{

    private final Consumer<B> consumerAdapter = new Consumer<B>(){
        public void consume(B b){
            ConsumeHandler.this.consume(B b);
        }
    };

    public void consume(A a){ //...
    }
    public void conusme(B b){ //...
    }
}

si un Consumer<A>est nécessaire, vous pouvez simplement passer this, et si Consumer<B>nécessaire, passez simplementconsumerAdapter

Rafael T
la source
La réponse de Daphna est la même, mais plus propre et moins alambiquée.
TWiStErRob
1

Vous ne pouvez pas le faire directement dans une classe car la définition de classe ci-dessous ne peut pas être compilée en raison de l'effacement des types génériques et de la déclaration d'interface en double.

class TwoTypesConsumer implements Consumer<Apple>, Consumer<Tomato> { 
 // cannot compile
 ...
}

Toute autre solution pour regrouper les mêmes opérations de consommation dans une classe nécessite de définir votre classe comme:

class TwoTypesConsumer { ... }

ce qui est inutile car vous devez répéter / dupliquer la définition des deux opérations et elles ne seront pas référencées depuis l'interface. IMHO faire cela est une mauvaise petite duplication de code que j'essaie d'éviter.

Cela peut également indiquer qu'il y a trop de responsabilité dans une classe pour consommer 2 objets différents (s'ils ne sont pas couplés).

Cependant, ce que je fais et ce que vous pouvez faire est d'ajouter un objet d'usine explicite pour créer des consommateurs connectés de la manière suivante:

interface ConsumerFactory {
     Consumer<Apple> createAppleConsumer();
     Consumer<Tomato> createTomatoConsumer();
}

Si en réalité ces types sont vraiment couplés (liés), je recommanderais de créer une implémentation de cette manière:

class TwoTypesConsumerFactory {

    // shared objects goes here

    private class TomatoConsumer implements Consumer<Tomato> {
        public void consume(Tomato tomato) {
            // you can access shared objects here
        }
    }

    private class AppleConsumer implements Consumer<Apple> {
        public void consume(Apple apple) {
            // you can access shared objects here
        }
    }


    // It is really important to return generic Consumer<Apple> here
    // instead of AppleConsumer. The classes should be rather private.
    public Consumer<Apple> createAppleConsumer() {
        return new AppleConsumer();
    }

    // ...and the same here
    public Consumer<Tomato> createTomatoConsumer() {
        return new TomatoConsumer();
    }
}

L'avantage est que la classe de fabrique connaît les deux implémentations, il existe un état partagé (si nécessaire) et vous pouvez renvoyer davantage de consommateurs couplés si nécessaire. Il n'y a pas de déclaration de méthode de consommation répétée qui ne soit pas dérivée de l'interface.

Veuillez noter que chaque consommateur peut être une classe indépendante (toujours privée) s'il n'est pas complètement lié.

L'inconvénient de cette solution est une complexité de classe plus élevée (même s'il peut s'agir d'un seul fichier java) et pour accéder à la méthode de consommation, vous avez besoin d'un appel supplémentaire au lieu de:

twoTypesConsumer.consume(apple)
twoTypesConsumer.consume(tomato)

vous avez:

twoTypesConsumerFactory.createAppleConsumer().consume(apple);
twoTypesConsumerFactory.createTomatoConsumer().consume(tomato);

Pour résumer, vous pouvez définir 2 consommateurs génériques dans une classe de premier niveau en utilisant 2 classes internes, mais en cas d'appel, vous devez d'abord obtenir une référence au consommateur d' implémentation approprié car il ne peut pas s'agir simplement d'un objet consommateur.

Kitarek
la source
1

Dans le style fonctionnel, il est assez facile de le faire sans implémenter l'interface et il effectue également la vérification du type au moment de la compilation.

Notre interface fonctionnelle pour consommer l'entité

@FunctionalInterface
public interface Consumer<E> { 
     void consume(E e); 
}

notre responsable pour traiter et consommer l'entité de manière appropriée

public class Manager {
    public <E> void process(Consumer<E> consumer, E entity) {
        consumer.consume(entity);
    }

    public void consume(Tomato t) {
        // Consume Tomato
    }

    public void consume(Apple a) {
        // Consume Apple
    }

    public void test() {
        process(this::consume, new Tomato());
        process(this::consume, new Apple());
    }
}
mano_ksp
la source
0

Une autre alternative pour éviter l'utilisation de plus de classes. (exemple utilisant java8 +)

// Mappable.java
public interface Mappable<M> {
    M mapTo(M mappableEntity);
}

// TwoMappables.java
public interface TwoMappables {
    default Mappable<A> mapableA() {
         return new MappableA();
    }

    default Mappable<B> mapableB() {
         return new MappableB();
    }

    class MappableA implements Mappable<A> {}
    class MappableB implements Mappable<B> {}
}

// Something.java
public class Something implements TwoMappables {
    // ... business logic ...
    mapableA().mapTo(A);
    mapableB().mapTo(B);
}
empreintes
la source
0

Désolé de répondre à de vieilles questions, mais j'aime vraiment ça! Essayez cette option:

public class MegaConsumer implements Consumer<Object> {

  Map<Class, Consumer> consumersMap = new HashMap<>();
  Consumer<Object> baseConsumer = getConsumerFor(Object.class);

  public static void main(String[] args) {
    MegaConsumer megaConsumer = new MegaConsumer();
    
    //You can load your customed consumers
    megaConsumer.loadConsumerInMapFor(Tomato.class);
    megaConsumer.consumersMap.put(Apple.class, new Consumer<Apple>() {
        @Override
        public void consume(Apple e) {
            System.out.println("I eat an " + e.getClass().getSimpleName());
        }
    });
    
    //You can consume whatever
    megaConsumer.consume(new Tomato());
    megaConsumer.consume(new Apple());
    megaConsumer.consume("Other class");
  }

  @Override
  public void consume(Object e) {
    Consumer consumer = consumersMap.get(e.getClass());
    if(consumer == null) // No custom consumer found
      consumer = baseConsumer;// Consuming with the default Consumer<Object>
    consumer.consume(e);
  }

  private static <T> Consumer<T> getConsumerFor(Class<T> someClass){
    return t -> System.out.println(t.getClass().getSimpleName() + " consumed!");
  }

  private <T> Consumer<T> loadConsumerInMapFor(Class<T> someClass){
    return consumersMap.put(someClass, getConsumerFor(someClass));
  }
}

Je pense que c'est ce que vous recherchez.

Vous obtenez cette sortie:

Tomate consommée!

Je mange une pomme

Chaîne consommée!

Awes0meM4n
la source
En question: "Mais je recherche la vérification de type au moment de la compilation ..."
aeracode
@aeracode Aucune option pour faire ce que OP veut. L'effacement de type rend impossible d'implémenter deux fois la même interface avec des variables de type différentes. J'essaye seulement de te donner une autre façon. Bien sûr, vous pouvez vérifier les types acceptés précédemment pour consommer un onbject.
Awes0meM4n