如何制作一个实现两种通用类型的接口的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)。
Lewis Diamond

勾选此功能性风格IMPL - stackoverflow.com/a/60466413/4121845
mano_ksp

Answers:


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
Jeff Axelrod

@JeffAxelrod我将使内部类成为非静态类,以便TwoTypesConsumer在必要时它们可以访问封闭的实例,然后可以传递twoTypesConsumer.getAppleConsumer()给需要Apple使用者的方法。另一种选择是添加类似于addConsumer(Producer<Apple> producer)TwoTypesConsumer的方法。
Herman 2012年

如果您无法控制界面(例如cxf / rs ExceptionMapper),则此方法将不起作用...
vikingsteve

17
我会说:这是Java 的缺陷。绝对没有理由不应该允许我们使用同一接口的多个实现,只要这些实现采用不同的参数即可。
gromit190 '17

41

由于类型擦除,您不能两次实现同一接口(使用不同的类型参数)。


6
我可以看到这是一个问题...然后的问题是,什么是绕过此问题的最佳方法(最有效,最安全,最优雅)。
daphshez

2
在不涉及业务逻辑的情况下,此处的“气味”类似于“访客”模式。
Shimi Bandiel 09年

12

这是基于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);
            }
        }
    }
}

问题的隐含要求是Consumer<Tomato>Consumer<Apple>对象共享状态。对Consumer<Tomato>, Consumer<Apple>对象的需求来自将这些作为参数的其他方法。我需要一个类来实现它们两者以便共享状态。

史蒂夫的想法是使用两个内部类,每个内部类实现一个不同的泛型类型。

此版本为实现Consumer接口的对象添加了getter,然后可以将其传递给期望它们的其他方法。


2
如果有人使用它:经常调用Consumer<*>实例,则值得将实例存储在实例字段中get*Consumer
TWiStErRob

7

至少,您可以通过执行以下操作来对分发的实现进行一些小的改进:

public class TwoTypesConsumer implements Consumer<Fruit> {

水果是番茄和苹果的祖先。


14
谢谢,但是无论专业人士怎么说,我都不认为番茄是水果。不幸的是,除了Object之外没有其他通用的基类。
daphshez

2
您始终可以创建一个名为:AppleOrTomato;)的基类
Shimi Bandiel,2009年

1
更好的是,添加一个代表Apple或Tomato的水果。
汤姆·霍顿

@Tom:除非我误会了您的意思,否则您的建议只会使问题向前发展,因为,要让Fruit能够委派给Apple或Tomato,Fruit必须拥有Apple和Tomato的超类领域引用它委托给的对象。
2009年

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>需要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

在Functional风格中,无需实现接口就很容易做到这一点,并且它还可以进行编译时类型检查。

我们使用实体的功能接口

@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

@aeracode没有选项可以执行OP想要的操作。类型擦除使得不可能使用不同的类型变量来两次实现相同的接口。我只想给你另一种方式。当然,您可以检查先前接受的消耗对象的类型。
Awes0meM4n
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.