Search code examples
javadictionarygenericseventshashmap

Java Event System Generic Cast to Bounded Wildcard


I was making my own Event System and tried to make it usable like this.

EventSystem.get(EventClientStart.class)
    .setCallback((event) -> {
        System.out.println("Client Started");
    });

EventSystem.get(EventClientStart.class)
    .invoke(new EventClientStart());

But got a problem with checks of type, what did I miss? T extends Event, I think it should work, and it actually is (only if I will cast it to (EventController), but then I will get warning "Unchecked Cast")

Intellij screenshot

EventSystem.java

package im.silkproject.event;

import im.silkproject.event.internal.EventController;

import java.util.HashMap;
import java.util.Map;

public final class EventSystem
{
    private static final Map<Class<? extends Event>, EventController<? extends Event>> map = new HashMap<>();

    private EventSystem() { }

    public static <T extends Event> EventController<T> get(Class<T> event)
    {
        return map.computeIfAbsent(event, k -> new EventController<>());
    }
}

Event.java

package im.silkproject.event;

public class Event
{
    private boolean cancelled;

    public void cancel()
    {
        cancelled = true;
    }

    public boolean isCancelled()
    {
        return cancelled;
    }
}

EventCallback.java

package im.silkproject.event.internal;

@FunctionalInterface
public interface EventCallback<T>
{
    void __call(T event);
}

EventController.java

package im.silkproject.event.internal;

import java.util.concurrent.CopyOnWriteArrayList;

public class EventController<T>
{
    private final CopyOnWriteArrayList<EventCallback<T>> callbacks = new CopyOnWriteArrayList<>();

    public void invoke(T event)
    {
        for (EventCallback<T> callback : callbacks)
        {
            callback.__call(event);
        }
    }

    public int length()
    {
        return callbacks.size();
    }

    public boolean setCallback(EventCallback<T> event)
    {
        return callbacks.addIfAbsent(event);
    }

    public boolean unsetCallback(EventCallback<T> event)
    {
        return callbacks.remove(event);
    }
}

Solution

  • Even though in your method you've declared that T is a subtype of Event and that the return type is EventController<T>, you're returning a Maps's value, which instead corresponds to EventController<? extends Event>>.

    At some point, T will refer to a very specific subtype of Event, while ? extends Event can be any unknown subtype of Event, and not necessarily correspond to T. In this scenario, the compiler cannot tell at compile time which exact type is returned for ? extends Event. So, since it isn't safe to return a ? extends Event in place of T, you're getting a compiler error.

    If you want to get rid of that error, the return type of your method should match the type of the Map's values.

    public static <T extends Event> EventController<? extends Event> get(Class<T> event) {
         return map.computeIfAbsent(event, k -> new EventController<>());
    }
    

    Here is also a very interesting article from Java Tutorials that introduces wildcards and explains your specific case with an example. I'll attach an extract here and highlight the part that applies to your case:

    public void drawAll(List<Shape> shapes) {
        for (Shape s: shapes) {
            s.draw(this);
       }
    }
    

    Now, the type rules say that drawAll() can only be called on lists of exactly Shape: it cannot, for instance, be called on a List<Circle>. That is unfortunate, since all the method does is read shapes from the list, so it could just as well be called on a List<Circle>. What we really want is for the method to accept a list of any kind of shape:

    public void drawAll(List<? extends Shape> shapes) {
        ...
    }
    

    There is a small but very important difference here: we have replaced the type List<Shape> with List<? extends Shape>. Now drawAll() will accept lists of any subclass of Shape, so we can now call it on a List<Circle> if we want.

    List<? extends Shape> is an example of a bounded wildcard. The ? stands for an unknown type, just like the wildcards we saw earlier. However, in this case, we know that this unknown type is in fact a subtype of Shape. (Note: It could be Shape itself, or some subclass; it need not literally extend Shape.) We say that Shape is the upper bound of the wildcard.

    There is, as usual, a price to be paid for the flexibility of using wildcards. That price is that it is now illegal to write into shapes in the body of the method. For instance, this is not allowed:

    public void addRectangle(List<? extends Shape> shapes) {
        // Compile-time error!
        shapes.add(0, new Rectangle());
    }
    

    You should be able to figure out why the code above is disallowed. The type of the second parameter to shapes.add() is ? extends Shape -- an unknown subtype of Shape. Since we don't know what type it is, we don't know if it is a supertype of Rectangle; it might or might not be such a supertype, so it isn't safe to pass a Rectangle there.