2

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.

Gikkman
  • 752
  • 6
  • 21
  • You can look up "type safe heterogeneous container". Basically you can't make this safe just by using generics, you have to rely on encapsulation. – Jorn Vernee Jul 23 '18 at 15:27
  • 1
    See https://stackoverflow.com/q/44422685/2891664 for some examples of what it looks to me like you're trying to do. (Also, I could be wrong about this, but I don't think this kind of thing is convenient to do in C++ either, unless you're trying to use the class-to-consumer map pattern as a kludgy substitute for C++ template specializations.) – Radiodef Jul 23 '18 at 15:34
  • Thanks for these pointers. I will take a read of these topics and see how I can rebuild this to be safe. I had never heard the term "type safe heterogeneous container", much appreciated! – Gikkman Jul 24 '18 at 10:36

3 Answers3

3

You're declaring void register(Consumers c) in BananaConsumer and its parameter is a raw type. You could do like this:

    public interface FruitConsumer<T extends Fruit> {
    void register(final Consumers<T> c);
    void consume(T fruit);
}

And when you implement FruitConsumer<Banana>, this parameter will be a banana consumer.

(I was explaining why that line works without reading all your code, and the solutions provided by other answers seems to be more logical for your problem.)

grape_mao
  • 1,153
  • 1
  • 8
  • 16
  • See also [*What is a raw type and why shouldn't we use it?*](https://stackoverflow.com/q/2770321/2891664) – Radiodef Jul 23 '18 at 15:38
  • I didn't realise I had a raw type there. Thanks for pointing it out. And thanks for the link @Radiodef, it was an interesting read. – Gikkman Jul 24 '18 at 10:37
2

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.

OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • Your right, this solution works well. Thank you for your effort. This solves the "how do I make it right" part. A few other answers provided me with pointers to why my code behaved the way it did, so all in all, I think I've gotten all the answers I need. Question: How do I go about choosing a correct answer then? Since the different answers answer different subquestions... – Gikkman Jul 24 '18 at 10:41
  • 1
    That's really your choice. In your position I would probably choose the answer that covered the *title* of the question best but by all means choose the one you found most helpful or pick any criteria you like. It's the upvotes that count most. – OldCurmudgeon Jul 24 '18 at 14:22
2

You can use a type token the way ClassToInstanceMap does:

A map, each entry of which maps a Java raw type to an instance of that type. In addition to implementing Map, the additional type-safe operations putInstance(java.lang.Class<T>, T) and getInstance(java.lang.Class<T>) are available.

So you could define a pair of methods that expose your registry like

<F extend Fruit> FruitConsumer<? super F> consumerFor(Class<F> fruitType);
<F extend Fruit> void registerConsumer(Class<F> fruitType, FruitConsumer<? super F> consumer);

You'd need to do an unchecked conversion on fetching from the internal map but it should be typesafe if all register calls are typesafe.

As long as you're not registering consumers for interface types, Consumers.consume could walk up the super-type chain to find an appropriate consumer.

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • Especially the part about "walking up the super-type chain" was a very useful insight, as it allowed me to solve a related problem "How do I add a listener for all types of Fruit". Very much appreciated! – Gikkman Jul 24 '18 at 10:42