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

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

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

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

Любые идеи?


person daphshez    schedule 19.08.2009    source источник
comment
Зачем вам нужны два универсальных интерфейса одного базового типа?   -  person akarnokd    schedule 19.08.2009
comment
Вы не можете этого сделать из-за стирания типа. Сохраните два разных класса, которые реализуют потребитель. Создает больше небольших классов, но сохраняет общий код (не используйте принятый ответ, он нарушает всю концепцию ... вы не можете рассматривать TwoTypesConsumer как потребителя, что является ПЛОХОЙ).   -  person Lewis Diamond    schedule 08.11.2013
comment
Проверьте это на наличие функционального стиля - stackoverflow.com/a/60466413/4121845   -  person mano_ksp    schedule 06.03.2020


Ответы (9)


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

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);
    }
}
person Steve McLeod    schedule 19.08.2009
comment
почему-то это выглядит как дублирование кода ... Я столкнулся с той же проблемой и не нашел другого решения, которое выглядело бы чистым. - person grackkle; 09.06.2011
comment
Но TwoTypesConsumer выполняет никаких контрактов, так в чем смысл? Его нельзя передать методу, которому требуется любой тип Consumer. Вся идея потребителя двух типов заключается в том, что вы можете передать его методу, который хочет потребителя помидоров, а также методу, который хочет потребителя яблок. Здесь нет ни того, ни другого. - person Jeff Axelrod; 11.08.2012
comment
@JeffAxelrod Я бы сделал внутренние классы нестатическими, чтобы они имели доступ к включающему экземпляру TwoTypesConsumer, если это необходимо, а затем вы можете передать twoTypesConsumer.getAppleConsumer() методу, которому нужен потребитель яблока. Другой вариант - добавить методы, подобные addConsumer(Producer<Apple> producer), в TwoTypesConsumer. - person herman; 27.09.2012
comment
Это не сработает, если у вас нет контроля над интерфейсом (например, cxf / rs ExceptionMapper) ... - person vikingsteve; 12.11.2013
comment
Я скажу это: это недостаток Java. Нет абсолютно никаких причин, по которым нам не должно быть позволено иметь несколько реализаций одного и того же интерфейса, при условии, что реализации принимают разные аргументы. - person birgersp; 08.05.2017

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

person Shimi Bandiel    schedule 19.08.2009
comment
Я понимаю, в чем проблема ... тогда вопрос в том, как лучше (наиболее эффективно, безопасно, элегантно) обойти эту проблему. - person daphshez; 19.08.2009
comment
Не вдаваясь в бизнес-логику, здесь что-то «пахнет» паттерном «Посетитель». - person Shimi Bandiel; 19.08.2009

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

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, которые затем могут быть переданы другим ожидающим их методам.

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

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

public class TwoTypesConsumer implements Consumer<Fruit> {

Фрукт является предком помидоров и яблок.

person Buhb    schedule 19.08.2009
comment
Спасибо, но что бы ни говорили профи, я не считаю помидор фруктом. К сожалению, нет общего базового класса, кроме Object. - person daphshez; 19.08.2009
comment
Вы всегда можете создать базовый класс под названием: AppleOrTomato;) - person Shimi Bandiel; 19.08.2009
comment
Лучше добавить Fruit, который делегирует Apple или Tomato. - person Tom Hawtin - tackline; 19.08.2009
comment
@Tom: Если я не неправильно понимаю, что вы говорите, ваше предложение только продвигает проблему вперед, поскольку, чтобы Fruit могла делегировать Apple или Tomato, Fruit должно иметь поле суперкласса как для Apple, так и для Tomato ссылаясь на объект, которому он делегирует. - person Buhb; 19.08.2009
comment
Это будет означать, что TwoTypesConsumer может потреблять любой тип Fruit, любой реализованный в настоящее время и любой, который может быть реализован в будущем. - person Tom Gillen; 05.03.2012
comment
@TomGillen Нет, если вы добавите checkConsume(Fruit fruit) или @throws CannotConsumeException if it can't consume the fruit. в комментарий javadoc для метода. - person AJMansfield; 12.11.2013

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

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

person Rafael T    schedule 22.09.2012
comment
Ответ Дафны такой же, но более чистый и менее запутанный. - person TWiStErRob; 24.04.2015

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

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

@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());
    }
}
person mano_ksp    schedule 29.02.2020

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

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

person kitarek    schedule 29.08.2016

Еще одна альтернатива, позволяющая избежать использования большего количества классов. (пример с использованием 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);
}
person fingerprints    schedule 07.01.2019

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

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

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

Вы получите такой результат:

Помидор съеден!

Я ем яблоко

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

person Awes0meM4n    schedule 18.01.2019
comment
Под вопросом: но я ищу проверку типов во время компиляции ... - person aeracode; 08.07.2019
comment
@aeracode Нет возможности делать то, что хочет OP. Стирание типа делает невозможным реализацию одного и того же интерфейса дважды с разными типами переменных. Я только пытаюсь дать вам другой путь. Конечно, вы можете проверить типы, принятые ранее, чтобы использовать onbject. - person Awes0meM4n; 10.07.2019