最近我在玩耍时遇到了这个问题。基本上,我想成为一个中心类,某种类型的消费者可以注册自己。然后,某些东西会为消费者发布对象,只有那些使用已发布类型的对象才可以接收它。
整个程序可以总结如下:
public class Main
{
public interface Fruit
{
}
public class Apple implements Fruit
{
}
public class Banana implements Fruit
{
}
public interface FruitConsumer<T extends Fruit>
{
public void consume(T fruit);
}
public class Consumers<T extends Fruit>
{
Map<Class<T>, Collection<FruitConsumer<T>>> consumers = new HashMap<>();
public void register(Class<T> clazz, FruitConsumer<T> consumer)
{
consumers.putIfAbsent(clazz, new HashSet<>());
consumers.get(clazz).add(consumer);
}
public void consume(T fruit)
{
Collection<FruitConsumer<T>> cons = consumers.get(fruit.getClass());
for (FruitConsumer<T> c : cons)
{
c.consume(fruit); // <-- Breaks if T is Apple
}
}
}
public class BananaConsumer implements FruitConsumer<Banana>
{
void register(Consumers c)
{
c.register(Banana.class, this);
c.register(Apple.class, this); // <-- Why does this work?
}
@Override
public void consume(Banana banana)
{
System.out.println("Mmm, banana");
}
}
public static void main(String... args)
{
Main main = new Main();
main.run();
}
private void run()
{
Consumers consumers = new Consumers<>();
BananaConsumer bananaConsumer = new BananaConsumer();
bananaConsumer.register(consumers);
Banana banana = new Banana();
consumers.consume(banana);
Apple apple = new Apple();
consumers.consume(apple);
}
}
现在,我想我明白为什么会崩溃。对于两个参数,编译器无法知道T
方法中的Consumers.register
是相同的T
。编译器只能强制两个参数都满足要求T extends Fruit
。我想记住能够在C ++中使用类似的代码结构,因此两种语言必须有所不同。我的假设在这里正确吗?
此外,解决此问题的正确方法是什么?我希望水果子类型的不同消费者能够注册自己,并且仅接收他们自己作为消费者的那些水果。另外,我希望您可以安全地将自己注册为消费者(我注意到,只要没有人将自己注册为“错误”,此代码将起作用)。
最后,这种现象的名字是什么?基本上,我可以通过Google搜索到更多有关此内容的信息。
答案 0 :(得分:3)
您在void register(Consumers c)
中声明了BananaConsumer
,其参数是原始类型。您可以这样:
public interface FruitConsumer<T extends Fruit> {
void register(final Consumers<T> c);
void consume(T fruit);
}
当您实现FruitConsumer<Banana>
时,此参数将成为香蕉使用者。
(我正在解释为什么该行在不阅读所有代码的情况下仍然有效,而其他答案提供的解决方案似乎更符合您的问题。)
答案 1 :(得分:2)
我认为您对仿制药的看法有点过头了。
您的Consumers
对象不需要是通用的,只需要拥有Map
即可。例如,您是否需要Consumers<Banana>
?
尝试一下:
public interface Fruit {
}
public class Apple implements Fruit {
}
public class Banana implements Fruit {
}
public interface FruitConsumer<T extends Fruit> {
void consume(T fruit);
}
public class Consumers {
Map<Class<? extends Fruit>, Collection<FruitConsumer<? extends Fruit>>> consumers = new HashMap<>();
public <T extends Fruit> void register(Class<T> clazz, FruitConsumer<T> consumer) {
consumers.putIfAbsent(clazz, new HashSet<>());
consumers.get(clazz).add(consumer);
}
public <T extends Fruit> void consume(T fruit) {
Collection<FruitConsumer<? extends Fruit>> cons = consumers.get(fruit.getClass());
for (FruitConsumer<? extends Fruit> con : cons) {
// Fair to cast here because we KNOW (because of the key) that it is the right type.
FruitConsumer<T> c = (FruitConsumer<T>)con;
c.consume(fruit);
}
}
}
public class BananaConsumer implements FruitConsumer<Banana> {
void register(Consumers c) {
c.register(Banana.class, this);
c.register(Apple.class, this); // <-- Now it breaks as expected.
}
@Override
public void consume(Banana banana) {
System.out.println("Mmm, banana");
}
}
现在还注意到意外允许的行为消失了。
答案 2 :(得分:2)
您可以像ClassToInstanceMap那样使用类型令牌:
一个映射,每个映射将Java原始类型映射到该类型的实例。除了实现
Map
,还可以使用其他类型安全操作putInstance(java.lang.Class<T>, T)
和getInstance(java.lang.Class<T>)
。
因此您可以定义一对公开注册表的方法,例如
<F extend Fruit> FruitConsumer<? super F> consumerFor(Class<F> fruitType);
<F extend Fruit> void registerConsumer(Class<F> fruitType, FruitConsumer<? super F> consumer);
从内部映射中获取数据时,您需要进行未经检查的转换,但是如果所有寄存器调用都是类型安全的,则应该是类型安全的。
只要您没有为接口类型注册使用者,Consumers.consume
可以沿超类型链查找合适的使用者。