Java - Map generic type to consumer of said type

Gikkman :

I ran into this problem recently when I was playing around. Basically, I wanted to make a central class where consumers of a certain type could register themselves. Something then publishes objects for the consumers, and only those consuming the published type should receive it.

The entire program can be summarised to this:

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

Now, I think I understand why this crashes. There is no way for the compiler to know that the T in the Consumers.registermethod is the same T, for both parameters. The compiler can only enforce that both arguments meets the requirement T extends Fruit. I want to remember being able to use a similar code structure in C++, so there has to be something the two languages does different. Is my assumption here correct?

Also, what would be the correct way of going about this? I would like different consumers of subtypes of Fruit to be able to register themselves, and receive only those fruits they enlist themselves as consumers of. Also, I would like some safety in what you can register yourself as a consumer off (I noticed that this code works as long as no one registers themselves "wrong").

Lastly, what is the name of this phenomena? Basically, what do I google to learn more about this.

OldCurmudgeon :

I think you've taken the generics a little too far.

Your Consumers object doesn't need to be generic, only the Map it holds. Are you ever going to need a Consumers<Banana> for example?

Try this:

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

Now also notice the unexpectedly allowed behavior goes away.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=113882&siteId=1