Is it possible to create my own event listener lis

2020-07-17 06:02发布

问题:

I'm implementing a client-server system where the client is in a continuous blocking read loop listening for messages from the server. When a message is received I'd like to raise an "event" based on the type of the message, which other GUI classes may add listeners to. I'm more familiar with C# events so I am still getting used to the Java way of doing things.

There will be many message types so I will need an interface for each, call it MessageTypeAListener, MessageTypeBListener, etc., each of which will contain one handle method, which my GUI classes will implement. However, there will be be many types and instead of maintaining a list of listeners per type and having several "fire" methods I wanted to have one big listener list and a typed fire method. Then the fire method could say "only fire listeners whose type is what I specify."

So for example (pseudocode):

ListenerList.Add(MessageTypeAListener); 
ListenerList.Add(MessageTypeBListener);

<T> fire(message) {
    ListenerList.Where(type is T).handle(message)
}

...  

fire<MessageTypeAListener>(message);

However, type erasure seems to be making this difficult. I could try casting and catching exceptions but that seems wrong. Is there a clean way of implementing this or is it just wiser to keep a separate list of listeners for every type, even though there will be tons of types?

回答1:

I implemented something like this, cause I have a visceral dislike of Java's EventListenerList. First, you implement a generic Listener. I defined the listener based upon the Event it was receiving, with basically one method

interface GenericListener<T extends Event> {
   public void handle(T t);
}

This saves you having to define ListenerA, ListernerB etc... Though you could do it your way with ListenerA, ListenerB, etc, all extending some base like MyListener. Both ways have plusses and minuses.

I then used a CopyOnWriteArraySet to hold all these listeners. A set is something to consider cause all too often listeners get added twice by sloppy coders. YMMV. But, effectively you have a Collection<GenericListener<T extends Event>> or a Collection<MyListener>

Now, as you've discovered, with type erasure, the Collection can only hold one type of listener. That is often a problem. Solution: Use a Map.

Since I'm basing everything upon the event, I used

Map<Class<T extends Event>, Collection<GenericListener<T extends Event>>>

based upon the class of the event, get the list of listeners who want to get that event.
Your alternative is to base it upon the class of the listener

Map<Class<T extends MyListener>, Collection<MyListener>>

There's probably some typos above...



回答2:

Old-fashioned pattern approach, using Visitor pattern:

class EventA {
    void accept(Visitor visitor) {
        System.out.println("EventA");
    }
}

class EventB {
    void accept(Visitor visitor) {
        System.out.println("EventB");
    }
}

interface Visitor {
    void visit(EventA e);
    void visit(EventB e);
}

class VisitorImpl implements Visitor {
    public void visit(EventA e) {
        e.accept(this);
    }

    public void visit(EventB e) {
        e.accept(this);
    }
}

public class Main {
    public static void main(String[] args) {
        Visitor visitor = new VisitorImpl();
        visitor.visit(new EventA());
    }
}

More modern approach is just to have Map between classes of events, which should not derive each other, and respective handlers of these events. This way you avoid disadvantages of Visitor pattern (which is, you'll need to change all your visitor classes, at least, base of them, every time you add new Event).

And another way is to use Composite pattern:

interface Listener {
    void handleEventA();
    void handleEventB();
}

class ListenerOne implements Listener {

    public void handleEventA() {
        System.out.println("eventA");
    }

    public void handleEventB() {
        // do nothing
    }
}

class CompositeListener implements Listener {
    private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<Listener>();
    void addListener(Listener l) {
        if (this != l)
            listeners.add(l);
    }

    public void handleEventA() {
        for (Listener l : listeners)
            l.handleEventA();
    }

    public void handleEventB() {
        for (Listener l : listeners)
            l.handleEventB();
    }
}


回答3:

After going through iterations of just about everyone's suggestions here, I ended up going a very slightly modified route of the standard Listener interfaces and listener lists. I started with Swing's EventListenerList, only to be disappointed with the amount of add/remove methods for dozens of message types. I realized this could not be condensed while still maintaining a single EventListenerList, so I started thinking about a separate list for each type. This makes it similar to .NET events where each event holds its own list of delegates to fire when raised. I wanted to avoid tons of add/remove methods, so I made a quick Event class that just looks like this:

public class Event<T extends EventListener> {
    private List<T> listeners = new ArrayList<T>();

    public void addListener(T listener) {
        listeners.add(listener);
    }

    public void removeListener(T listener) {
        listeners.remove(listener);
    }

    public List<T> getListeners() {
        return listeners;
    }
}

Then I keep several instances of this class around, each typed according to a listener, so Event<MessageTypeAListener>, etc. My classes can then call the add method to add themselves to that particular event. I would've like to be able to call a generic Raise method on the Event instance to then fire all the handlers, but I did not want them to all have to have the same "handle" method, so this was not possible. Instead, when I'm ready to fire the listeners, I just do

  for (MessageTypeAListener listener : messageTypeAEvent.getListeners())
      listener.onMessageTypeA(value);

I'm sure this is not a new idea and has probably been done before and in better/more robust ways, but it's working great for me and I'm happy with it. Best of all, it's simple.

Thanks for all the help.



回答4:

If you only have simple events, i.e. events without data or where all events have the same data types, enum could be a way forward:

public enum Event {
    A,
    B,
    C
}

public interface EventListener {
    void handle(Event event);
}

public class EventListenerImpl implements EventListener {
    @Override
    public void handle(Event event) {
        switch(event) {
            case A: 
                // ...
                break;
        }
    }
}

public class EventRegistry {
    private final Map<Event, Set<EventListener>> listenerMap;

    public EventRegistry() {
        listenerMap = new HashMap<Event, Set<EventListener>>();
        for (Event event : Event.values()) {
            listenerMap.put(event, new HashSet<EventListener>());
        }
    }

    public void registerEventListener(EventListener listener, Event event) {
        Set<EventListener> listeners = listenerMap.get(event);
        listeners.add(listener);
    }

    public void fire(Event event) {
        Set<EventListener> listeners = listenerMap.get(event);
        for (EventListener listener : listeners) {
            listener.handle(event);
        }
    }
}

Comments:

The switch statement in the EventListnerImpl may be omitted if it is only registered to a single event, or if it should always act in the same way regardless of which Event it receives.

The EventRegister has stored the EventListener(s) in a map, meaning that each listener will only get the kind of Event(s) that it has subscribed to. Additionally, the EventRegister uses Sets, meaning that an EventListener will only receive the event at most once (to prevent that the listener will receive two events if someone accidentally registers the listener twice).