Java-将通用类型映射到所述类型的使用者

时间:2018-07-23 15:13:45

标签: java generics

最近我在玩耍时遇到了这个问题。基本上,我想成为一个中心类,某种类型的消费者可以注册自己。然后,某些东西会为消费者发布对象,只有那些使用已发布类型的对象才可以接收它。

整个程序可以总结如下:

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搜索到更多有关此内容的信息。

3 个答案:

答案 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可以沿超类型链查找合适的使用者。