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

164

Ich habe eine generische Schnittstelle

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

Ich habe eine Klasse, die zwei Arten von Objekten verwendet, also möchte ich etwas tun wie:

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

Anscheinend kann ich das nicht machen.

Ich kann den Versand natürlich selbst durchführen, 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 bin jedoch auf der Suche nach der Lösung für die Typprüfung und den Versand zur Kompilierungszeit, die Generika bieten.

Die beste Lösung, die ich mir vorstellen kann, besteht darin, separate Schnittstellen zu definieren, 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?

Daphshez
quelle
Warum benötigen Sie zwei generische Schnittstellen desselben Basistyps?
Akarnokd
6
Aufgrund der Typlöschung ist dies nicht möglich. Behalten Sie zwei verschiedene Klassen bei, die Consumer implementieren. Macht mehr kleine Klassen, hält aber Ihren Code generisch (Verwenden Sie nicht die akzeptierte Antwort, es bricht das gesamte Konzept ... Sie können den TwoTypesConsumer nicht als Verbraucher behandeln, was SCHLECHT ist).
Lewis Diamond
Überprüfen Sie dies für den Funktionsstil impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Antworten:

78

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 das Erstellen dieser statischen inneren Klassen stört, 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);
    }
}
Steve McLeod
quelle
2
Irgendwie sieht das wie eine Codeduplizierung aus ... Ich bin auf dasselbe Problem gestoßen und habe keine andere Lösung gefunden, die sauber aussieht.
Bln-Tom
109
Aber TwoTypesConsumererfüllt keine Verträge, also was ist der Sinn? Es kann nicht an eine Methode übergeben werden, die einen der beiden Typen möchte Consumer. Die ganze Idee eines Zwei-Typ-Verbrauchers wäre, dass Sie es einer Methode geben können, die einen Tomatenkonsumenten will, sowie einer Methode, die einen Apfelkonsumenten will. Hier haben wir keine.
Jeff Axelrod
@JeffAxelrod Ich würde die inneren Klassen nicht statisch machen, damit sie TwoTypesConsumerbei Bedarf Zugriff auf die einschließende Instanz haben, und dann können Sie twoTypesConsumer.getAppleConsumer()an eine Methode übergeben, die einen Apple-Konsumenten haben möchte. Eine andere Möglichkeit wäre, Methoden hinzuzufügen, addConsumer(Producer<Apple> producer)die TwoTypesConsumer ähneln.
Hermann
Dies funktioniert nicht, wenn Sie keine Kontrolle über die Schnittstelle haben (z. B. cxf / rs ExceptionMapper) ...
vikingsteve
17
Ich werde es sagen: Dies ist ein Fehler bei Java. Es gibt absolut keinen Grund, warum wir nicht mehrere Implementierungen derselben Schnittstelle haben dürfen, vorausgesetzt, die Implementierungen verwenden unterschiedliche Argumente.
Gromit190
41

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

Shimi Bandiel
quelle
6
Ich kann sehen, wie es ein Problem ist ... die Frage ist dann, was der beste (effizienteste, sicherste, eleganteste) Weg ist, um dieses Problem zu umgehen.
Daphshez
2
Ohne auf die Geschäftslogik einzugehen, "riecht" hier etwas nach dem Besuchermuster.
Shimi Bandiel
12

Hier ist eine mögliche Lösung, die auf Steve McLeods basiert :

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 war Consumer<Tomato>und Consumer<Apple>Objekte, die den Status teilen. Der Bedarf an Consumer<Tomato>, Consumer<Apple>Objekten ergibt sich aus anderen Methoden, die diese als Parameter erwarten. Ich brauche eine Klasse, die beide implementiert, um den Status zu teilen.

Steves Idee war es, zwei innere Klassen zu verwenden, die jeweils einen anderen generischen Typ implementieren.

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.

Daphshez
quelle
2
Wenn jemand dies verwendet: Es lohnt sich, die Consumer<*>Instanzen in Instanzfeldern zu speichern, wenn get*Consumersie häufig aufgerufen werden.
TWiStErRob
7

Zumindest können Sie Ihre Implementierung des Versands geringfügig verbessern, indem Sie Folgendes tun:

public class TwoTypesConsumer implements Consumer<Fruit> {

Obst ist ein Vorfahr von Tomaten und Äpfeln.

Buhb
quelle
14
Danke, aber was auch immer die Profis sagen, ich betrachte Tomate nicht als Frucht. Leider gibt es außer Object keine gemeinsame Basisklasse.
Daphshez
2
Sie können jederzeit eine Basisklasse namens AppleOrTomato erstellen;)
Shimi Bandiel
1
Fügen Sie besser eine Frucht hinzu, die entweder an Apple oder an Tomate delegiert.
Tom Hawtin - Tackline
@ Tom: Wenn ich nicht falsch verstehe, was Sie sagen, treibt Ihr Vorschlag das Problem nur voran, da Obst, um an Apple oder Tomate delegieren zu können, ein Feld einer Superklasse sowohl für Apple als auch für Tomate haben muss Verweisen auf das Objekt, an das es delegiert.
Buhb
1
Dies würde bedeuten, dass TwoTypesConsumer jede Art von Obst konsumieren kann, jede derzeit implementierte und jede andere, die möglicherweise in Zukunft implementiert wird.
Tom Gillen
3

bin nur darüber gestolpert. Es ist einfach passiert, dass ich das gleiche Problem hatte, aber ich habe es auf eine andere Weise gelöst: Ich habe gerade eine neue Schnittstelle wie diese 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 jede Logik angesehen. Sie müssen also einen kleinen Adapter für den zweiten Verbraucher wie diesen 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 a Consumer<A>benötigt wird, können Sie einfach bestehen this, und wenn Consumer<B>es benötigt wird, bestehen Sie einfachconsumerAdapter

Rafael T.
quelle
Daphnas Antwort ist dieselbe, aber sauberer und weniger verworren.
TWiStErRob
1

Sie können dies nicht direkt in einer Klasse tun, da die folgende Klassendefinition aufgrund des Löschens generischer Typen und der doppelten Schnittstellendeklaration nicht kompiliert werden kann.

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

Für jede andere Lösung zum Packen derselben Verbrauchsoperationen in eine Klasse muss Ihre Klasse wie folgt definiert werden:

class TwoTypesConsumer { ... }

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

Dies kann auch ein Indikator dafür sein, dass eine Klasse zu viel Verantwortung trägt, um zwei verschiedene Objekte zu verbrauchen (wenn sie nicht gekoppelt sind).

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

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

Wenn diese Typen in Wirklichkeit wirklich gekoppelt (verwandt) sind, würde ich empfehlen, eine Implementierung folgendermaßen 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, es einen gemeinsamen Status gibt (falls erforderlich) und Sie bei Bedarf mehr gekoppelte Verbraucher zurückgeben können. Es gibt keine wiederholte Deklaration der Verbrauchsmethode, die nicht von der Schnittstelle abgeleitet ist.

Bitte beachten Sie, dass jeder Verbraucher eine unabhängige (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 es sich um eine Java-Datei handeln kann). Um auf die Konsummethode zuzugreifen, benötigen Sie einen weiteren Aufruf, anstatt:

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

du hast:

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

Zusammenfassend kann man definieren 2 allgemeine Verbraucher in einer Top-Level - Klasse 2 innere Klassen verwenden , aber bei Aufruf müssen Sie zunächst einen Verweis erhalten auf geeignete Umsetzung der Verbraucher , da dies nicht nur ein Verbraucher Objekt sein kann.

Kitarek
quelle
1

Im funktionalen Stil ist dies recht einfach, ohne die Schnittstelle zu implementieren, und es wird auch die Überprüfung des Kompilierungszeittyps durchgeführt.

Unsere funktionale Schnittstelle zum Konsumieren von Entitäten

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

unser Manager, um Entität angemessen zu verarbeiten und zu konsumieren

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
quelle
0

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);
}
Fingerabdrücke
quelle
0

Entschuldigung für die Beantwortung alter 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 es, wonach Sie suchen.

Sie erhalten diese Ausgabe:

Tomate verbraucht!

Ich esse einen Apfel

String verbraucht!

Awes0meM4n
quelle
In Frage: "Aber ich suche nach der Typprüfung zur Kompilierungszeit ..."
Aeracode
@aeracode Keine Optionen, um das zu tun, was OP will. Durch das Löschen von Typen kann dieselbe Schnittstelle nicht zweimal mit unterschiedlichen Typvariablen implementiert werden. Ich versuche nur, dir einen anderen Weg zu geben. Natürlich können Sie zuvor akzeptierte Typen überprüfen, um ein Objekt zu verbrauchen.
Awes0meM4n