简体   繁体   中英

Dynamic type-casting

I am playing with a Java 8 event bus where a register method takes an event class and a Java 8 function reference, something like: -

class SomeSubscriber {
    SomeSubscriber(EventBus eventBus) {
        eventBus.register(MyEvent.class, this::onMyEvent);
        eventBus.register(SomeOtherEvent.class, this::onSomeOtherEvent);
        eventBus.register(YetAnotherEvent.class, this::onYetAnotherEvent);
    }

    private void onMyEvent(MyEvent e) {
        ... do something with the MyEvent ...
    }

    private void onSomeOtherEvent(SomeOtherEvent e) {
        ... do something with the SomeOtherEvent ...
    }

    private void onYetAnotherEvent (YetAnotherEvent e) {
        ... do something with the YetAnotherEvent ...
    }
}

Publishers can simply post to the same bus: -

class SomePublisher {
    SomePublisher(EventBus eventBus) {
        eventBus.post(new MyEvent(...));
    }
}

My (very simplistic at the moment) EventBus currently looks like this: -

public class EventBus {
    private final Map<Class<? extends EventBase>, List<Handler<? extends EventBase>>> subscribers;

    public EventBus() {
        subscribers = new HashMap<>();
    }

    public <T extends EventBase> void register(Class<? extends EventBase> eventClass, Handler<T> handler) {
        List<Handler<? extends EventBase>> typeSubs =
                subscribers.computeIfAbsent(
                        eventClass,
                        (e) -> new ArrayList<Handler<? extends EventBase>>());

        typeSubs.add(handler);
    }

    public <T extends EventBase> void post(T event) {
        List<Handler<? extends EventBase>> typeSubs = subscribers.get(event.getClass());
        for (Handler<? extends EventBase> handler : typeSubs) {
            handler.handleEvent((? extends EventBase)event.getClass().asSubclass(event.getClass()));
        }
    }
}

My problem is with the post method - it needs to somehow dynamically cast to what the target needs, ie MyEvent in the example above. I can't seem to write the post method to cast the event or otherwise to satisfy the compiler. Even though the event being passed in (from SomePublisher is a MyEvent instance, the post method is not capable of retaining that information.

What do I need to do to achieve this dynamic type casting. Reflection? MethodHandles?

Also, I'd ideally not require the EventBase marker interface.

Thanks

PS. existing event bus libraries are "good-enough" but using lambda's for this intrigues me and strikes me as compact and direct.

An option that seems to work is to make event base look like this

public interface EventBase {
    default <T extends EventBase> void accept(Handler<T> handler) {
        handler.handleEvent((T) this);
    }
}

and then have your event bus dispatch back through it like this.

public <T extends EventBase> void post(T event) {
    List<Handler<? extends EventBase>> typeSubs = subscribers.get(event.getClass());
    for (Handler<? extends EventBase> handler : typeSubs) {
        event.accept(handler);
    }
}

Makes event base a bit messy though, so i'm not sure how I feel about it. But it will pass a test based on your sample code.

EDIT : I simplified EventBase a little, but i can't seem to get rid of the unchecked cast.

Insight into the problem

Unfortunately, I think that what you're trying to do may not be possible. The trouble lies with this: you know that the event you're giving the handler has a class compatible with what the handler can accept, but only as a result of the fact that you retrieved the handlers from the Map you set up which holds the handlers by their event type. The compiler , however, doesn't understand this logic. From its perspective, you're trying to take an arbitrary handler, which is expecting some specific extension of event base that the compiler doesn't know and giving it an event which may or may not fit the expected type. Perhaps it would be helpful to think in terms of the question: Even if I could magically change the code so that when an event of type MyEvent was passed in the compiler cast it to a MyEvent before giving it to the handler, how would the compiler know that the handler could accept a MyEvent ? All the compiler knows is that the handler accepts something specific extending from EventBase.

Looking at the bolded question above, it becomes more clear why any reflective solution would fail. You may be able to use reflection to cast the event to its appropriate class, but the compiler doesn't know whether that class works for the handler.

A Workaround

I recommend Iscoughlin's solution of a default method in EventBase to flip around the dependency so that you give the event the handler rather than giving the handler the event. This fits much more cleanly into your model than the workaround I am about to suggest. But for the sake of completeness, here is another (admittedly less clean) solution:

public interface Handler {
    public void handleEvent(EventBase event);
}

The bus:

public class EventBus {
    private final Map<Class<? extends EventBase>, List<Handler>> subscribers;

    public EventBus() {
        subscribers = new HashMap<>();
    }

    public void register(Class<? extends EventBase> eventClass, Handler handler) {
        List<Handler> typeSubs =
                subscribers.computeIfAbsent(
                        eventClass,
                        (e) -> new ArrayList<Handler>());

        typeSubs.add(handler);
    }

    public <T extends EventBase> void post(T event) {
        List<Handler> typeSubs = subscribers.get(event.getClass());
        for (Handler handler : typeSubs) {
            handler.handleEvent(event);
        }
    }
}

Subscribers would look like this (here's where this solution is less-than-ideal):

class SomeSubscriber {
    SomeSubscriber(EventBus eventBus) {
        eventBus.register(MyEvent.class, this::onMyEvent);
        eventBus.register(SomeOtherEvent.class, this::onSomeOtherEvent);
        eventBus.register(YetAnotherEvent.class, this::onYetAnotherEvent);
    }

    private void onMyEvent(EventBase e) {
        MyEvent event = (MyEvent)e;
        //... do something with the MyEvent ...
    }

    private void onSomeOtherEvent(EventBase e) {
        SomeOtherEvent event = (SomeOtherEvent)e;
        //... do something with the SomeOtherEvent ...
    }

    private void onYetAnotherEvent (EventBase e) {
        YetAnotherEvent event = (YetAnotherEvent)e;
        //... do something with the YetAnotherEvent ...
    }
}

I have succeeded in my original aims by simply using reflection. For anybody's interest, the code follows. The key problem I had (casting event object) is nicely side-stepped by Method.invoke(Object obj, Object ...args) taking an Object for arguments - no need for casting.

package experiments.eventbus;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventBus {
    private static String handlerMethodName;
    private final Map<Class<?>, List<HandlerMethod>> handlerMethods;

    static {
        Class<Handler> c = Handler.class;
        handlerMethodName = c.getMethods()[0].getName();
    }

    public EventBus() {
        handlerMethods = new HashMap<>();
    }

    public <T> void register(Class<T> eventClass, Handler<T> handler) {
        List<HandlerMethod> handlers = handlerMethods.computeIfAbsent(eventClass, (e) -> new ArrayList<HandlerMethod>());
        Method method = lookupMethod(handler);
        handlers.add(new HandlerMethod(handler, method));
    }

    public <T> void post(T event) {
        List<HandlerMethod> handlers = handlerMethods.get(event.getClass());

        if (handlers == null) {
            return;
        }

        for (HandlerMethod handler : handlers) {
            handler.invoke(event);
        }
    }

    private <T> Method lookupMethod(Handler<T> handler) {
        Method[] methods = handler.getClass().getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals(handlerMethodName)) {
                return method;
            }
        }

        // This isn't possible, but need to satisfy the compiler
        throw new RuntimeException();
    }

    /**
     * Tuple of a Handler<?> (functional interface provided by subscriber) and a {@link Method} to that function (that
     * can be invoked with an "Object" event, i.e. Method#invoke takes an Object.
     */
    private static class HandlerMethod {
        private final Handler<?> handler;
        private final Method method;

        HandlerMethod(Handler<?> handler, Method method) {
            this.handler = handler;
            this.method = method;
        }

        void invoke(Object event) {
            try {
                method.invoke(handler, event);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

The updated version has a little extra stuff, like looking up the handler method from the Handler implementation and a tuple for holding the handler instance and the corresponding Method .

If I had more time, I would investigate the difference using MethodHandle - maybe quicker. My solution will probably be slower than using the defender method in the EventBus.

Note: my solution is clearly incomplete, no comments, no tests, no unregister method, etc, etc.

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