如何制作一个实现了两个泛型类型的接口的Java类?

179

我有一个通用接口。

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
由于类型擦除,你不能这样做。保持两个不同的类实现Consumer接口。这会增加更多的小类,但是保持你的代码通用化(不要使用已经被接受的答案,它破坏了整个概念...你不能将TwoTypesConsumer视为Consumer,这是不好的)。 - Lewis Diamond
请查看此链接以获取函数式实现 - https://dev59.com/lnM_5IYBdhLWcg3wn0rT#60466413 - mano_ksp
9个回答

79
考虑封装:
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
这似乎是代码重复...我遇到了同样的问题,没有找到其他干净的解决方案。 - grackkle
125
但是 TwoTypesConsumer 没有履行任何契约,那意义在哪里呢?它无法传递给需要任一类型的 Consumer 的方法。一个双类型的 consumer 的整个想法就是你可以将它提供给既需要 tomato consumer 又需要 apple consumer 的方法。但是这里都没有。 - Jeff Axelrod
@JeffAxelrod 我会将内部类设置为非静态,这样它们就可以访问封闭的 TwoTypesConsumer 实例(如果需要的话),然后您可以将 twoTypesConsumer.getAppleConsumer() 传递给需要苹果消费者的方法。另一个选择是向 TwoTypesConsumer 添加类似于 addConsumer(Producer<Apple> producer) 的方法。 - herman
如果您无法控制接口(例如cxf/rs ExceptionMapper),则此方法无效... - vikingsteve
29
我来翻译:我直言不讳:这是Java的一个缺陷。只要实现采用不同的参数,我们完全可以允许有多个相同接口的实现,没有任何理由不允许这样做。 - birgersp
当您使用收集器对象收集流的元素时,会发生此情况。 因为收集可能会被并行化,所以收集器必须接受单个元素和相同类型的另一个收集器。这意味着收集器必须实现 Consumer < Element >Consumer < Collector > ,因此我们应该能够将其声明为实现相应的接口。 - Christopher Yeleighton

44

由于类型擦除的原因,您无法使用不同类型参数实现相同的接口。


6
我可以理解这是一个问题……问题在于什么是最好的(最有效、安全、优雅)方式来规避这个问题。 - daphshez
2
不涉及业务逻辑,这里有些东西“闻起来”像访问者模式。 - Shimi Bandiel

12
以下是基于 Steve McLeod 的解决方案的可能解决方法:

这里是一个基于 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>对象来作为其他方法的参数。我需要一个类来同时实现它们以便共享状态。
Steve的想法是使用两个内部类,每个内部类实现不同的泛型类型。
这个版本为实现Consumer接口的对象添加了getter方法,然后可以将它们传递给其他期望它们的方法。

2
如果有人使用这个:如果经常调用get*Consumer,那么将Consumer<*>实例存储在实例字段中是值得的。 - TWiStErRob

7

至少,你可以通过以下方式对调度的实现进行小改进:

public class TwoTypesConsumer implements Consumer<Fruit> {

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

16
谢谢,但无论专家们怎么说,我不认为番茄是水果。不幸的是,除了Object以外,没有共同的基类。 - daphshez
2
您总是可以创建一个名为:AppleOrTomato 的基类 ;) - Shimi Bandiel
1
最好的方法是添加一个叫做“水果”的类,可以委派给苹果或番茄。 - Tom Hawtin - tackline
@Tom:除非我误解了你的意思,否则你的建议只是把问题推迟了,因为为了让Fruit能够委托给Apple或Tomato,Fruit必须有一个超类字段,既可以引用Apple也可以引用Tomato所委托的对象。 - Buhb
1
这意味着TwoTypesConsumer可以消费任何类型的水果,包括当前已实现的和未来可能实现的任何类型。 - Tom Gillen
@TomGillen 如果你在方法的javadoc注释中添加一个checkConsume(Fruit fruit)或者@throws CannotConsumeException如果无法消耗水果。,那就不会出现这种情况了。 - AJMansfield

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

1
Daphna的回答是相同的,但更加简洁明了。 - TWiStErRob

3

在函数式编程中,不需要实现接口也可以很容易地完成这个任务,并且它可以在编译时进行类型检查。

我们的函数式接口用于消费实体。

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

2

由于泛型类型的擦除和重复接口声明,您无法在一个类中直接执行此操作,因为下面的类定义不能被编译。

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

优点在于工厂类知道两种实现方式,有共享状态(如果需要),并且如果需要可以返回更紧密耦合的消费者。没有重复的 consume 方法声明,这些方法不是从接口派生出来的。
请注意,如果消费者彼此之间没有完全相关,则每个消费者可能是独立的(仍为私有)类。
这种解决方案的缺点是类的复杂性更高(即使这可以是一个 Java 文件),并且要访问 consume 方法,您需要再调用一次,所以不是:
twoTypesConsumer.consume(apple)
twoTypesConsumer.consume(tomato)

你拥有:

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

总结一下,您可以使用2个内部类在一个顶层类中定义2个通用的消费者,但在调用时,您需要首先获取适当的实现消费者的引用,因为这不能简单地是一个消费者对象。


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

我认为这就是你在寻找的东西。
你会得到以下输出:

吃掉了一个番茄!

我吃了一个苹果

消耗了字符串!


1
但我正在寻找编译时类型检查... - aeracode
@aeracode 没有选项可以满足 OP 的要求。类型擦除使得无法使用不同的类型变量实现相同的接口两次。我只是试图给你另一种方式。当然,你可以检查之前接受的类型以消耗一个对象。 - Awes0meM4n

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接