Jak stworzyć klasę Java, która implementuje jeden interfejs z dwoma typami ogólnymi?

164

Mam ogólny interfejs

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

Mam klasę, która zużywa dwa typy obiektów, więc chciałbym zrobić coś takiego:

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

Najwyraźniej nie mogę tego zrobić.

Mogę oczywiście samodzielnie zrealizować wysyłkę np

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

Ale szukam rozwiązania do sprawdzania typów i wysyłania w czasie kompilacji, które zapewniają produkty generyczne.

Najlepszym rozwiązaniem, jakie przychodzi mi do głowy, jest zdefiniowanie oddzielnych interfejsów, np

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

Wydaje mi się, że funkcjonalnie to rozwiązanie jest w porządku. Jest po prostu gadatliwy i brzydki.

Jakieś pomysły?

daphshez
źródło
Dlaczego potrzebujesz dwóch ogólnych interfejsów tego samego typu bazowego?
akarnokd
6
Ze względu na wymazywanie typu nie możesz tego zrobić. Zachowaj dwie różne klasy, które implementują konsumenta. Tworzy więcej małych klas, ale zachowuje ogólny kod (nie używaj zaakceptowanej odpowiedzi, to łamie całą koncepcję ... nie możesz traktować TwoTypesConsumer jako konsumenta, co jest ZŁE).
Lewis Diamond
Sprawdź to, aby uzyskać funkcjonalny styl. Impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Odpowiedzi:

78

Rozważ hermetyzację:

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

Jeśli przeszkadza Ci tworzenie statycznych klas wewnętrznych, możesz użyć klas anonimowych:

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
źródło
2
w jakiś sposób wygląda to na powielanie kodu ... Napotkałem ten sam problem i nie znalazłem innego rozwiązania, które wyglądałoby na czyste.
bln-tom
109
Ale nieTwoTypesConsumer spełnia żadnych umów, więc po co? Nie można go przekazać do metody, która potrzebuje dowolnego typu Consumer. Cała idea konsumenta dwojakiego polega na tym, że można go podarować metodzie, która chce konsumenta pomidorów, a także metodzie, która chce konsumenta jabłek. Tutaj nie mamy żadnego.
Jeff Axelrod,
@JeffAxelrod Uczyniłbym klasy wewnętrzne niestatyczne, aby w TwoTypesConsumerrazie potrzeby miały dostęp do otaczającej instancji, a następnie można było przejść twoTypesConsumer.getAppleConsumer()do metody, która chce konsumenta Apple. Inną opcją byłoby dodanie metod podobnych addConsumer(Producer<Apple> producer)do TwoTypesConsumer.
herman,
To nie działa, jeśli nie masz kontroli nad interfejsem (np. Cxf / rs ExceptionMapper) ...
vikingsteve
17
Powiem tak: to błąd w Javie. Nie ma absolutnie żadnego powodu, dla którego nie powinniśmy mieć wielu implementacji tego samego interfejsu, pod warunkiem, że implementacje przyjmują różne argumenty.
gromit190
41

Ze względu na wymazywanie typów nie można dwukrotnie zaimplementować tego samego interfejsu (z różnymi parametrami typu).

Shimi Bandiel
źródło
6
Widzę, jak to jest problem ... Pytanie brzmi więc, jaki jest najlepszy (najbardziej skuteczny, bezpieczny, elegancki) sposób na obejście tego problemu.
daphshez,
2
Bez wchodzenia w logikę biznesową, coś tutaj „pachnie” jak wzór Odwiedzający.
Shimi Bandiel,
12

Oto możliwe rozwiązanie oparte na rozwiązaniu 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);
            }
        }
    }
}

Implikowanym wymaganiem pytania było Consumer<Tomato>i Consumer<Apple>obiekty, które mają wspólny stan. Potrzeba Consumer<Tomato>, Consumer<Apple>obiektów wynika z innych metod, które oczekują ich jako parametrów. Potrzebuję jednej klasy, aby wdrożyć je obie w celu udostępnienia stanu.

Pomysł Steve'a polegał na użyciu dwóch klas wewnętrznych, z których każda implementowała inny typ ogólny.

Ta wersja dodaje metody pobierające dla obiektów, które implementują interfejs konsumenta, które mogą być następnie przekazywane do innych metod, które ich oczekują.

daphshez
źródło
2
Jeśli ktoś tego używa: warto przechowywać Consumer<*>instancje w polach instancji, jeśli get*Consumerjest często wywoływany.
TWiStErRob
7

Przynajmniej możesz dokonać niewielkich ulepszeń w implementacji wysyłki, wykonując coś takiego:

public class TwoTypesConsumer implements Consumer<Fruit> {

Owoc będący przodkiem Pomidora i Jabłka.

Buhb
źródło
14
Dzięki, ale cokolwiek mówią profesjonaliści, nie uważam pomidora za owoc. Niestety nie ma innej wspólnej klasy bazowej niż Object.
daphshez,
2
Zawsze możesz utworzyć klasę bazową o nazwie: AppleOrTomato;)
Shimi Bandiel
1
Lepiej dodaj owoc, który jest delegowany do jabłka lub pomidora.
Tom Hawtin - tackline,
@Tom: O ile nie rozumiem źle tego, co mówisz, twoja sugestia tylko popycha problem do przodu, ponieważ aby Fruit mógł delegować na Jabłko lub Pomidor, Owoce muszą mieć pole nadklasy zarówno dla Apple, jak i Pomidora odwołując się do obiektu, do którego deleguje.
Buhb,
1
Oznaczałoby to, że TwoTypesConsumer może spożywać każdy rodzaj Owoców, każdego aktualnie wdrożonego i każdego, kto może zaimplementować w przyszłości.
Tom Gillen,
3

po prostu się na to natknąłem. Tak się złożyło, że miałem ten sam problem, ale rozwiązałem go w inny sposób: właśnie stworzyłem nowy interfejs, taki jak ten

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

niestety jest to uważane Consumer<A>za Consumer<B>sprzeczne z całą logiką. Musisz więc stworzyć mały adapter dla drugiego konsumenta w swojej klasie

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

jeśli Consumer<A>jest potrzebne, możesz po prostu przejść this, a jeśli Consumer<B>to konieczne, po prostu przejśćconsumerAdapter

Rafael T.
źródło
Odpowiedź Daphny jest taka sama, ale czystsza i mniej zawiła.
TWiStErRob
1

Nie można tego zrobić bezpośrednio w jednej klasie, ponieważ poniższa definicja klasy nie może zostać skompilowana z powodu usunięcia typów ogólnych i zduplikowanej deklaracji interfejsu.

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

Każde inne rozwiązanie pakowania tych samych operacji konsumpcji w jednej klasie wymaga zdefiniowania Twojej klasy jako:

class TwoTypesConsumer { ... }

co jest bezcelowe, ponieważ musisz powtórzyć / powielić definicję obu operacji i nie będzie do nich odwołań z interfejsu. IMHO robienie tego jest złym, małym i duplikatem kodu, którego staram się uniknąć.

Może to wskazywać również na to, że w jednej klasie jest zbyt duża odpowiedzialność za zużywanie 2 różnych obiektów (jeśli nie są one połączone).

Jednak to, co robię i co możesz zrobić, to dodać jawny obiekt fabryki, aby utworzyć połączonych konsumentów w następujący sposób:

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

Jeśli w rzeczywistości te typy są naprawdę sprzężone (powiązane), to polecam stworzyć implementację w taki sposób:

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

Zaletą jest to, że klasa fabryczna zna obie implementacje, istnieje stan współdzielony (w razie potrzeby) i w razie potrzeby można zwrócić więcej połączonych konsumentów. Nie ma powtarzającej się deklaracji metody konsumpcji, która nie pochodzi z interfejsu.

Należy pamiętać, że każdy konsument może być niezależną (nadal prywatną) klasą, jeśli nie jest w pełni spokrewniony.

Wadą tego rozwiązania jest złożoność wyższej klasy (nawet jeśli może to być jeden plik java) i aby uzyskać dostęp do metody consume, potrzebujesz jeszcze jednego wywołania, więc zamiast:

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

ty masz:

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

Podsumowując, możesz zdefiniować 2 ogólnych konsumentów w jednej klasie najwyższego poziomu przy użyciu 2 klas wewnętrznych, ale w przypadku wywołania musisz najpierw uzyskać odniesienie do odpowiedniego konsumenta implementującego, ponieważ nie może to być po prostu jeden obiekt konsumenta.

Kitarek
źródło
1

W stylu funkcjonalnym jest to dość łatwe bez implementowania interfejsu, a także sprawdza typ czasu kompilacji.

Nasz funkcjonalny interfejs do konsumpcji bytu

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

nasz menedżer do odpowiedniego przetwarzania i konsumowania podmiotu

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
źródło
0

Inna alternatywa, aby uniknąć stosowania większej liczby klas. (przykład przy użyciu 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);
}
odciski palców
źródło
0

Przepraszam, że odpowiadam na stare pytania, ale naprawdę to uwielbiam! Wypróbuj tę opcję:

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

Myślę, że właśnie tego szukasz.

Otrzymasz ten wynik:

Spożyty pomidor!

Jem jabłko

Zużyty sznurek!

Awes0meM4n
źródło
W pytaniu: „Ale ja szukam sprawdzania typu w czasie kompilacji ...”
aeracode,
@aeracode Brak opcji robienia tego, czego chce OP. Wymazywanie typów uniemożliwia implementację tego samego interfejsu dwukrotnie z różnymi zmiennymi typu. Próbuję tylko dać ci inny sposób. Oczywiście możesz sprawdzić typy zaakceptowane wcześniej, aby konsumować obiekt.
Awes0meM4n,