web-dev-qa-db-de.com

Wie erstelle ich eine Java-Klasse, die eine Schnittstelle mit zwei generischen Typen implementiert?

Ich habe eine generische Schnittstelle

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

Ich habe eine Klasse, die zwei Arten von Objekten verbraucht, daher möchte ich Folgendes tun:

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

Anscheinend kann ich das nicht.

Ich kann den Versand natürlich selbst implementieren, z.

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

Ich suche jedoch nach der Kompilierzeit-Typüberprüfungs- und Dispatching-Lösung, die Generics bieten.

Die beste Lösung, die ich mir vorstellen kann, ist die Definition separater Schnittstellen, z.

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

Funktionell ist diese Lösung in Ordnung, denke ich. Es ist nur wortreich und hässlich.

Irgendwelche Ideen? 

146
daphshez

Betrachten Sie die Kapselung:

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) {  .....  }
    }
}

Wenn Sie diese statischen inneren Klassen erstellen, können Sie anonyme Klassen verwenden:

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);
    }
}
70
Steve McLeod

Aufgrund der Typenlöschung können Sie dieselbe Schnittstelle nicht zweimal implementieren (mit unterschiedlichen Typparametern).

34
Shimi Bandiel

Hier ist eine mögliche Lösung basierend auf Steve McLeods :

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

Die implizite Anforderung der Frage waren Consumer<Tomato>- und Consumer<Apple>-Objekte, die den Status gemeinsam nutzen. Consumer<Tomato>, Consumer<Apple>-Objekte werden von anderen Methoden benötigt, die diese als Parameter erwarten. Ich brauche eine Klasse, die beide implementiert, um den Zustand zu teilen. 

Steves Idee bestand darin, zwei innere Klassen zu verwenden, von denen jede einen anderen generischen Typ implementierte. 

Diese Version fügt Getter für die Objekte hinzu, die die Consumer-Schnittstelle implementieren, die dann an andere Methoden übergeben werden können, die sie erwarten.

9
daphshez

Zumindest können Sie Ihre Implementierung von Versand ein wenig verbessern, indem Sie Folgendes tun:

public class TwoTypesConsumer implements Consumer<Fruit> {

Frucht, die ein Vorfahr der Tomate und des Apfels ist.

7
Buhb

stolperte gerade darüber. Es war einfach so, dass ich das gleiche Problem hatte, aber ich habe es auf eine andere Art gelöst: Ich habe gerade eine neue Schnittstelle erstellt

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

leider wird dies als Consumer<A> und NICHT als Consumer<B> gegen alle Logic betrachtet. Sie müssen also einen kleinen Adapter für den zweiten Verbraucher in Ihrer Klasse erstellen

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

wenn ein Consumer<A> benötigt wird, können Sie einfach this übergeben, und wenn Consumer<B> benötigt wird, übergeben Sie einfach consumerAdapter.

3
Rafael T

Sie können dies nicht direkt in einer Klasse durchführen, da die folgende Klassendefinition nicht kompiliert werden kann, weil generische Typen gelöscht und Schnittstellendeklaration doppelt vorhanden ist. 

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

Für jede andere Lösung zum Verpacken derselben Verbrauchsvorgänge in einer Klasse müssen Sie Ihre Klasse folgendermaßen definieren:

class TwoTypesConsumer { ... }

dies ist sinnlos, da Sie die Definition beider Vorgänge wiederholen/duplizieren müssen und sie nicht von der Schnittstelle referenziert werden. IMHO ist dies eine schlechte, kleine Code-Duplizierung, die ich zu vermeiden versuche.

Dies ist möglicherweise ein Hinweis darauf, dass in einer Klasse zu viel Verantwortung besteht, um 2 verschiedene Objekte zu verbrauchen (wenn sie nicht gekoppelt sind). 

Was ich jedoch mache und was Sie tun können, ist das explizite Factory-Objekt hinzuzufügen, um verbundene Verbraucher auf folgende Weise zu erstellen:

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

Wenn diese Typen in der Realität wirklich miteinander gekoppelt sind, würde ich empfehlen, eine Implementierung so zu erstellen:

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();
    }
}

Der Vorteil ist, dass die Factory-Klasse beide Implementierungen kennt, dass es einen gemeinsamen Status gibt (falls erforderlich) und Sie ggf. mehr gekoppelte Verbraucher zurückgeben können. Es gibt keine sich wiederholende Verbrauchsmethode-Deklaration, die nicht von der Schnittstelle abgeleitet ist.

Bitte beachten Sie, dass jeder Verbraucher eine unabhängige (immer noch private) Klasse sein kann, wenn er nicht vollständig verwandt ist.

Der Nachteil dieser Lösung ist eine höhere Komplexität der Klasse (auch wenn dies eine Java-Datei sein kann). Um auf die Verbrauchsmethode zuzugreifen, benötigen Sie einen weiteren Aufruf. Statt also:

twoTypesConsumer.consume(Apple)
twoTypesConsumer.consume(tomato)

du hast:

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

Zusammenfassend können Sie definieren 2 generische Consumer in einer Top-Level-Klasse mit 2 inneren Klassen, aber wenn Sie anrufen, müssen Sie zunächst einen Verweis auf den entsprechenden Implementierungs - Consumer erhalten, da dies nicht einfach ein Consumer-Objekt sein kann .

1
kitarek

Sorry für alte Fragen, aber ich liebe es wirklich! Versuchen Sie diese 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));
  }
}

Ich denke, das ist, wonach du suchst.

Sie erhalten diese Ausgabe:

Tomate verbraucht!

Ich esse einen Apfel

String verbraucht!

0
Awes0meM4n

Eine weitere Alternative, um die Verwendung weiterer Klassen zu vermeiden. (Beispiel mit 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);
}
0
winter