Как сделать класс Java, который реализует один интерфейс с двумя универсальными типами?

164

У меня общий интерфейс

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

У меня есть класс, который использует два типа объектов, поэтому я хотел бы сделать что-то вроде:

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

Видимо, я не могу этого сделать.

Конечно, я могу осуществить отправку самостоятельно, например

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

Но я ищу решение для проверки и диспетчеризации типов во время компиляции, которое предоставляют дженерики.

Лучшее решение, которое я могу придумать, - это определить отдельные интерфейсы, например

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

Функционально, это решение хорошо, я думаю. Это просто многословно и безобразно.

Любые идеи?

daphshez
источник
Зачем вам два универсальных интерфейса одного базового типа?
akarnokd
6
Из-за стирания типа вы не можете этого сделать. Держите это двумя разными классами, которые реализует потребитель. Делает больше небольших классов, но сохраняет ваш код универсальным (не используйте принятый ответ, это нарушает всю концепцию ... вы не можете рассматривать TwoTypesConsumer как потребителя, который является ПЛОХОЙ).
Льюис Даймонд
Проверьте это для функционального стиля impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Ответы:

78

Рассмотрим инкапсуляцию:

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

Если создание этих статических внутренних классов вас беспокоит, вы можете использовать анонимные классы:

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);
    }
}
Стив Маклеод
источник
2
почему-то это выглядит как дублирование кода ... Я столкнулся с той же проблемой и не нашел другого решения, которое выглядит чисто.
Bln-Tom
109
Но не TwoTypesConsumerвыполняет никаких контрактов, так какой смысл? Это не может быть передано методу, который хочет любой тип Consumer. Вся идея потребителя двух типов заключается в том, что вы можете передать его методу, который хочет потребителя томатов, а также методу, который хочет потребителя яблок. Здесь у нас нет ни того, ни другого.
Джефф Аксельрод
@JeffAxelrod Я бы сделал внутренние классы нестатичными, чтобы они имели доступ к включающему TwoTypesConsumerэкземпляру при необходимости, а затем вы можете перейти twoTypesConsumer.getAppleConsumer()к методу, который хочет потребителя Apple. Другой вариант - добавить методы, похожие addConsumer(Producer<Apple> producer)на TwoTypesConsumer.
herman
Это не работает, если у вас нет контроля над интерфейсом (например, cxf / rs ExceptionMapper) ...
vikingsteve
17
Я скажу это: это недостаток Java. Нет абсолютно никаких причин, по которым нам нельзя разрешать иметь несколько реализаций одного и того же интерфейса, при условии, что реализации принимают разные аргументы.
gromit190
41

Из-за стирания типа вы не можете реализовать один и тот же интерфейс дважды (с разными параметрами типа).

Шими Бандиэль
источник
6
Я вижу, как это проблема ... вопрос в том, каков наилучший (самый эффективный, безопасный, элегантный) способ обойти эту проблему.
Дафшез
2
Не вдаваясь в бизнес-логику, что-то здесь «пахнет», как образец посетителя.
Шими Бандиэль
12

Вот возможное решение на основе решения Стива Маклеода :

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

Неявным требованием этого вопроса были Consumer<Tomato>и Consumer<Apple>объекты, которые разделяют состояние. Потребность в Consumer<Tomato>, Consumer<Apple>объектах исходит от других методов, которые ожидают их в качестве параметров. Мне нужен один класс, чтобы реализовать их оба, чтобы поделиться государством.

Идея Стива состояла в том, чтобы использовать два внутренних класса, каждый из которых реализовывал свой общий тип.

В этой версии добавлены методы получения объектов, реализующих интерфейс Consumer, которые затем можно передать другим ожидающим их методам.

daphshez
источник
2
Если кто-то использует это: стоит хранить Consumer<*>экземпляры в полях экземпляров, если get*Consumerвызывается часто.
TWiStErRob
7

По крайней мере, вы можете сделать небольшое улучшение в своей реализации диспетчеризации, выполнив что-то вроде следующего:

public class TwoTypesConsumer implements Consumer<Fruit> {

Фрукты, являющиеся предками помидоров и яблок.

Buhb
источник
14
Спасибо, но что бы ни говорили профессионалы, я не считаю помидор фруктом. К сожалению, нет общего базового класса, кроме Object.
Дафшез
2
Вы всегда можете создать базовый класс под названием: AppleOrTomato;)
Шими Бандиэль
1
Лучше, добавьте Фрукт, который делегирует или Яблоку или Помидору.
Том Хотин -
@Tom: Если я неправильно понимаю то, что вы говорите, ваше предложение только продвигает проблему вперед, поскольку для того, чтобы Fruit мог делегировать Apple или Tomato, у Fruit должно быть поле суперкласса для Apple и Tomato ссылаясь на объект, которому он делегирует.
Буб
1
Это будет означать, что TwoTypesConsumer может потреблять любой тип Fruit, любой реализованный в настоящее время и любой кто-либо может реализовать в будущем.
Том Гиллен
3

просто наткнулся на это. Случилось так, что у меня возникла та же проблема, но я решил ее по-другому: я просто создал новый интерфейс, подобный этому

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

к сожалению, это считается как Consumer<A>и НЕ Consumer<B>против всей логики. Таким образом, вы должны создать небольшой адаптер для второго потребителя, как это в вашем классе

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

если Consumer<A>требуется, вы можете просто пройти this, а если Consumer<B>нужно, просто передатьconsumerAdapter

Рафаэль Т
источник
Дафна ответ тот же, но чище и менее запутанный.
TWiStErRob
1

Вы не можете сделать это напрямую в одном классе, так как приведенное ниже определение класса не может быть скомпилировано из-за удаления универсальных типов и дублирования объявления интерфейса.

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

Любое другое решение для упаковки одинаковых операций потребления в одном классе требует определения вашего класса как:

class TwoTypesConsumer { ... }

что бессмысленно, так как вам нужно повторять / дублировать определение обеих операций, и они не будут ссылаться из интерфейса. ИМХО это плохое маленькое дублирование кода, которого я пытаюсь избежать.

Это также может быть показателем того, что в одном классе слишком много ответственности, чтобы потреблять 2 разных объекта (если они не связаны).

Однако, что я делаю и что вы можете сделать, это добавить явный объект фабрики для создания подключенных потребителей следующим образом:

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

Если в действительности эти типы действительно связаны (связаны), я бы рекомендовал создать реализацию следующим образом:

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

Преимущество состоит в том, что фабричный класс знает обе реализации, существует общее состояние (если необходимо), и вы можете вернуть больше связанных потребителей, если это необходимо. Нет повторяющегося объявления метода потребления, который не является производным от интерфейса.

Обратите внимание, что каждый потребитель может быть независимым (все еще частным) классом, если он не полностью связан.

Недостатком этого решения является более высокая сложность класса (даже если это может быть один Java-файл), и для доступа к методу потребления вам потребуется еще один вызов, вместо:

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

у тебя есть:

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

Подводя итог, вы можете определить 2 общих потребителя в одном классе верхнего уровня, используя 2 внутренних класса, но в случае вызова вам нужно сначала получить ссылку на соответствующего реализующего потребителя, поскольку это не может быть просто один объект потребителя.

kitarek
источник
1

В функциональном стиле это довольно легко сделать без реализации интерфейса, а также он выполняет проверку типов во время компиляции.

Наш функциональный интерфейс для использования сущности

@FunctionalInterface
public interface Consumer<E> { 
     void consume(E 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
источник
0

Еще одна альтернатива, чтобы избежать использования большего количества классов. (пример использования 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

Извините за ответы на старые вопросы, но мне это очень нравится! Попробуйте этот вариант:

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

Я думаю, это то, что вы ищете.

Вы получаете этот вывод:

Томат потребляется!

Я ем яблоко

Строка израсходована!

Awes0meM4n
источник
На вопрос: «Но я ищу проверку типов во время компиляции ...»
aeracode
@aeracode Нет вариантов делать то, что хочет OP. Стирание типа делает невозможным реализацию одного и того же интерфейса дважды с разными типами переменных. Я только пытаюсь дать вам другой путь. Конечно, вы можете проверить типы, принятые ранее, чтобы использовать объект.
Awes0meM4n