简体   繁体   中英

How to use generic collections with typed parameters in a generic map with wildcards (Java)

This question is related to How to create and fire a collection of Consumers<T> in java for an event listener pattern . I was trying to use a collection of events to help batch operations up, but so far I can't seem to come up with the correct configuration of generic definitions to work.

Here is my code so far.

private ConcurrentHashMap<Class<? extends Event>, ConcurrentLinkedQueue<Consumer<Collection<? extends Event>>>> listeners;

public <T extends Event> void listen(Class<T> clazz, Consumer<Collection<T>> consumer){
    ConcurrentLinkedQueue<Consumer<Collection<T>>> consumers = listeners.get(clazz);
    if (consumers == null) {
        consumers = new ConcurrentLinkedQueue<>();
        listeners.put(clazz, consumers); // Complains that consumers is not the type Collection<? extends Event>
    }
    consumers.add(consumer);
}

public <T extends Event> void fire(Collection<T> eventToFire){
    ConcurrentLinkedQueue<Consumer<Collection<? extends Event>>> consumers = listeners.get(eventToFire.getClass());
    if(consumers != null){
        consumers.forEach(x -> x.accept(eventToFire));
    }
}

I've tried setting ConcurrentLinkedQueue<Consumer<Collection<T>>> consumers = listeners.get(clazz); to be ConcurrentLinkedQueue<Consumer<Collection<? extends Event>>> consumers = listeners.get(clazz); ConcurrentLinkedQueue<Consumer<Collection<? extends Event>>> consumers = listeners.get(clazz); but then it complains that the consumers.add(consumer); is not the type Consumer<Collection<T>> .

No casting seems to be getting around this, with the exception of consumers.add((Consumer<Collection<? extends Event>>)(Object)consumer); , but this just looks horrible to me, and it seems like I must be missing something, or messing something up, because it doesn't seem like this should be a required step.

No, that's pretty much how it works. Java's type system can't express the constraint you're trying to express. Your wrapper implementation is type-safe, but the compiler can't prove it, so you'll have to do dirty unsafe casts to convince the compiler you know what you're doing.

You want the compiler to know that if the key is of type Class<T> (where T extends Event ), then the value is a ConcurrentLinkedQueue<Consumer<Collection<T>>> .

Unfortunately, there is no way to express this fact. However, you know it's true because the only key/value pairs you ever put into the map have that form. Therefore it's safe to put in the cast, but I would also include a comment explaining why it's ok.

The type of the values should actually be

ConcurrentLinkedQueue<? extends Consumer<? extends Collection<? extends Event>>>

not

ConcurrentLinkedQueue<Consumer<Collection<? extends Event>>>

The latter is a queue of Consumers that must be able to accept a collection of any type extending Event , but you only want a Consumer that accepts Collections of some specific type T extending Event . It's incredibly difficult to understand this, but just remember that you usually need ? extends ? extends at every level when you mix wildcards with nested type parameters.

Finally, eventToFire.getClass() isn't what you want as this returns the class of some collection type, whereas your keys are of type Class<? extends Event> Class<? extends Event> . Due to type erasure, you cannot get the type parameter from a collection at runtime, so you will need to explicitly pass clazz to fire as well.

With these changes the code becomes

private ConcurrentHashMap<Class<? extends Event>, ConcurrentLinkedQueue<? extends Consumer<? extends Collection<? extends Event>>>> listeners;

public <T extends Event> void listen(Class<T> clazz, Consumer<Collection<T>> consumer){
    ConcurrentLinkedQueue<Consumer<Collection<T>>> consumers = (ConcurrentLinkedQueue<Consumer<Collection<T>>>) listeners.get(clazz);
    if (consumers == null) {
        consumers = new ConcurrentLinkedQueue<>();
        listeners.put(clazz, consumers);
    }
    consumers.add(consumer);
}

public <T extends Event> void fire(Class<T> clazz, Collection<T> eventToFire){
    ConcurrentLinkedQueue<Consumer<Collection<T>>> consumers = (ConcurrentLinkedQueue<Consumer<Collection<T>>>) listeners.get(clazz);
    if(consumers != null){
        consumers.forEach(x -> x.accept(eventToFire));
    }
}

The only casts required are in the two lines that assume the key and value match, but this is the best you can possibly do.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM