Як зробити клас 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);
}

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

Будь-які ідеї?


Для чого потрібні два загальні інтерфейси одного і того ж базового типу?
akarnokd

6
Через стирання типу ви не можете цього зробити. Тримайте це два різні класи, які реалізує споживач. Створює більше невеликих класів, але зберігає ваш код загальним (Не використовуйте прийняту відповідь, це порушує всю концепцію ... ви не можете ставитися до TwoTypesConsumer як до споживача, який є BAD).
Льюїс Діамант

Перевірте це для функціональних осущ стилю - 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()що додається , і тоді ви можете перейти до методу, який хоче споживач яблук. Іншим варіантом було б додати методи, схожі addConsumer(Producer<Apple> producer)на TwoTypesConsumer.
герман

Це не працює, якщо у вас немає контролю над інтерфейсом (наприклад, 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>об'єктах походить від інших методів, які очікують їх як параметрів. Мені потрібен один клас реалізувати їх обох, щоб поділитися станом.

Ідея Стіва полягала у використанні двох внутрішніх класів, кожен із яких реалізував різний родовий тип.

Ця версія додає гетьтери для об’єктів, що реалізують споживчий інтерфейс, які потім можуть бути передані іншим методам, які очікують їх.


2
Якщо хтось використовує це: варто зберігати Consumer<*>екземпляри в полях екземплярів, якщо get*Consumerвони викликаються часто.
TWiStErRob

7

Принаймні, ви можете зробити невелике покращення щодо вашої реалізації відправки, зробивши щось на зразок наступного:

public class TwoTypesConsumer implements Consumer<Fruit> {

Фрукт є родоначальником томата та яблука.


14
Дякую, але як би там не говорили, я не вважаю Томат фруктом. На жаль, немає іншого загального базового класу, крім Object.
дафшез

2
Ви завжди можете створити базовий клас під назвою: AppleOrTomato;)
Шимі Бандіель,

1
Краще, додайте фрукт, який делегується або Apple, або помідор.
Том Хотін - тайклін

@Tom: Якщо я не розумію те, що ви говорите, ваша пропозиція лише висуває проблему вперед, оскільки для того, щоб Fruit міг делегувати або Apple, або помідор, Fruit повинен мати поле суперкласу як для Apple, так і для помідорів. посилаючись на об'єкт, який він делегує.
Бух

1
Це означає, що TwoTypesConsumer може вживати будь-які фрукти, будь-які реалізовані в даний час і будь-хто, хто може реалізувати їх у майбутньому.
Том Гіллен

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

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

Це може бути показником також того, що в одному класі є занадто велика відповідальність за споживання двох різних об'єктів (якщо вони не пов'язані між собою).

Однак те, що я роблю, і те, що ви можете зробити, - це додати явний заводський об’єкт для створення підключених споживачів наступним чином:

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 внутрішніх класи, але у випадку виклику вам потрібно спочатку звернутися до відповідного споживача, що впроваджує, оскільки це не може бути просто одним об’єктом споживача.


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

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

Я думаю, що це те, що ти шукаєш.

Ви отримуєте цей вихід:

Помідор вживається!

Я їм яблуко

Рядок споживається!


Питання: "Але я шукаю перевірку типу компіляції ..."
аерокод

@aeracode Немає варіантів робити те, що хоче ОП. Стирання типу робить неможливим двічі реалізувати один і той же інтерфейс з різними змінними типу. Я лише намагаюся дати тобі інший шлях. Звичайно, ви можете перевірити типи, прийняті раніше, щоб споживати об'єкт.
Awes0meM4n
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.