How cast to (known) generic type without "unchecked" warning?

OneBigOwnage :

I have these two interfaces;

public interface Event {
    default void dispatch() {
        EventBus.getInstance().dispatch(this);
    }
}
public interface EventListener<T extends Event> {
    void handle(T event);
}

If I understand generics in Java correctly, I'm practically telling the inheritor of the second interface to put th

Then I came up with the next this piece of code, where listeners can be registered, events can be thrown and registered listeners will handle any thrown event.

public class EventBus {

    /**
     * The singleton EventBus instance.
     */
    private static EventBus instance;

    /**
     * The map of event types and their listeners.
     */
    private final Map<Class<? extends Event>, Set<EventListener<? extends Event>>> listeners = new ConcurrentHashMap<>();

    /**
     * Create a new EventBus instance.
     */
    private EventBus() {

    }

    /**
     * Retrieve the singleton bus instance.
     *
     * @return The event bus instance.
     */
    public static EventBus getInstance() {
        if (instance == null) {
            instance = new EventBus();
        }

        return instance;
    }

    /**
     * Register a new event listener, that listens to the given event type.
     *
     * @param <T>
     * @param type     The type of event that the given listener should react on.
     * @param listener The listener that we want to register.
     */
    public <T extends Event> void registerListener(Class<T> type, EventListener<T> listener) {
        Set<EventListener<? extends Event>> eventListeners = getOrCreateListeners(type);

        eventListeners.add(listener);
    }

    /**
     * Retrieve a set of listeners, either by retrieving an existing list or by creating a new one.
     *
     * @param eventClass The type of event for which to retrieve listeners.
     *
     * @return A set of event listeners that listen for the given event.
     */
    private Set<EventListener<? extends Event>> getOrCreateListeners(Class<? extends Event> eventClass) {
        Set<EventListener<? extends Event>> eventSubscribers = listeners.get(eventClass);

        if (eventSubscribers == null) {
            eventSubscribers = new CopyOnWriteArraySet<>();
            listeners.put(eventClass, eventSubscribers);
        }

        return eventSubscribers;
    }

    /**
     * Dispatch the given event to all registered listeners associated with that type.
     *
     * @param <T>
     * @param event The event that is to be dispatched.
     */
    public <T extends Event> void dispatch(T event) {
        listeners.keySet().stream()
                .filter(type -> type.isAssignableFrom(event.getClass()))
                .flatMap(type -> listeners.get(type).stream())
                .forEach(listener -> {
                    try {
                        ((EventListener<T>) listener).handle(event); //  <-- This is where the compiler warns me...
                    } catch (Exception e) {
                        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                    }
                });
    }
}

The line throwing a warning is near the bottom: ((EventListener<T>) listener).handle(event);

The code for the event listener is loosely based on this piece of code. Sadly, that piece doesn't make use of generics whatsoever. When I add separate interfaces for event and listener, a lot of rawtype and unchecked warnings appear in the code. I started turning a few of the methods and the listeners Map into generic ones. I have been fiddling around with question marks, Ts and much more trying to figure this out. I've already learned a lot about generics from coding this but I can't seem to figure this one out.


I think that the answer can be found in either a) turning the listeners Map generic (somehow?): I want to tell the compiler that the type of event in the EventListener<? extends Event> is of the type that that Class<? extends Event> describes. Or, b) creating a "safe" cast on the line that gives a warning.

I tried the first option by doing this (and some more attempts):

/**
 * The map of event types and their listeners.
 */
private final Map<Class<T extends Event>, Set<EventListener<T>>> listeners = new ConcurrentHashMap<>();

But without much luck, as the compiler will tell you.

I also attempted the second option by adding the following if-statement (a few more attempts were made here as well):

if (listener instanceof EventListener<T>) {
    ((EventListener<T>) listener).handle(event); //  <-- This is where the compiler warns me...
}

This won't work either, as the type of T will be erased at runtime...

Maybe I'm close, but just not using the correct syntax. Maybe it's not even possible for me to pass the correct information to the compiler or preserve it in the runtime. Maybe I am not even on the right track...


So far I've browsed issues like this, this and this one, sadly without much luck.

Kayaman :

Unfortunately while you know that .filter(type -> type.isAssignableFrom(event.getClass())) filters out unsuitable types, the compiler doesn't (and can't) know it, hence the warning.

Your map of listeners has generic parameters that just won't allow you to keep full (compile time) type safety. The wildcards make sure of that. You know (at compile-time) that the map contains listeners for some events as values, but as soon as you put a listener in there, you can't take it out anymore without losing compile-time type safety.

Map<Class<? extends Event>, Set<EventListener<? extends Event>>> listeners

So what are your options? Well, you know that it is runtime typesafe after you check the event type, so you might just want to tag a @SuppressWarnings annotation on the method and move on. After all, it's just a warning to make sure you know what you're doing.

Guess you like

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