在Java中,是否可以创建包含多个监听器类型的自定义事件监听器列表?

4
我正在实现一个客户端-服务器系统,在该系统中,客户端处于连续的阻塞读取循环中,监听来自服务器的消息。当接收到消息时,我想根据消息类型引发一个“事件”,其他GUI类可能会添加侦听器。我更熟悉C#事件,所以我仍在适应Java做事情的方式。
由于会有许多消息类型,因此我需要为每种类型创建一个接口,称之为MessageTypeAListener、MessageTypeBListener等,每个接口将包含一个处理方法,我的GUI类将实现该方法。然而,将会有许多类型,而不是针对每种类型维护一个侦听器列表和几个“触发”方法,我想要一个大的侦听器列表和一个具有类型的触发方法。然后,触发方法可以说“只触发我指定类型的侦听器”。
例如(伪代码):
ListenerList.Add(MessageTypeAListener); 
ListenerList.Add(MessageTypeBListener);

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

...  

fire<MessageTypeAListener>(message);

然而,类型擦除似乎使此变得困难。我可以尝试强制转换并捕获异常,但这似乎不正确。是否有一种清晰的实现方式,或者保留每种类型的单独监听器列表更为明智,即使会有大量的类型?


请查看EventListenerList - 它可以容纳任意数量的侦听器类型的侦听器。 - Nate W.
谢谢-那个类看起来正好符合我的需求。这是用于Android的,该类在本地不可用,不过我可以轻松获取源代码并将其包含在我的项目中。 - sou
你不能使用 listener.class,而是必须使用 listener.getClass(),它将返回监听器对象(DefaultButtonListener)的运行时类型,而不是监听器接口类型(IButtonListener)。 - Nate W.
我明白了。这意味着,尽管有一个组合的监听器列表,但我仍需要为每种类型的监听器分别编写添加/删除方法,以便可以显式地使用ListenerType.class。一旦我拥有数十种消息和监听器类型,这将导致很多冗余代码,但我猜这部分无法避免,是吗? - sou
是的,您可能需要创建所有这些方法。 - Nate W.
显示剩余3条评论
4个回答

2

我实现了类似这样的功能,因为我对Java的EventListenerList有一种强烈的不喜欢。首先,您需要实现一个通用的Listener接口。我基于接收到的Event定义了Listener,它只有一个方法。

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

这样可以省去定义ListenerA、ListenerB等的麻烦。尽管你也可以按照自己的方式定义ListenerA、ListenerB等,让它们都扩展某个基类,比如MyListener。这两种方法各有优缺点。

接着我使用了CopyOnWriteArraySet来保存所有这些监听器。使用集合是值得考虑的,因为经常会有粗心的程序员添加重复的监听器。但是,你可以有效地拥有一个Collection<GenericListener<T extends Event>>或者Collection<MyListener>。

现在,正如你发现的那样,由于类型擦除,Collection只能保存一种类型的监听器。这通常是一个问题。解决方案:使用Map。

由于我以事件为基础构建了所有内容,因此我使用了

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

根据事件的类别,获取想要接收该事件的侦听器列表。
您也可以基于侦听器的类别。

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

There's probably some typos above...


请注意,您甚至不需要说T扩展事件。在这种情况下,您可以广播除事件之外的对象,例如异常、数据等。 - user949300
假设你有FirstEventSecondEvent(都继承自Event)。如果你想同时监听这两个事件,是不是需要使用MyListener implements GenericListener<Event>并且使用不太美观的instanceof来实现呢?或者是否有更优雅的解决方案?显然,public class MyListener implements GenericListener<FirstEvent>, GenericListener<SecondEvent> 是行不通的 :-( - matsev
地图(Map)使得广播多个事件类型并为每种类型设立侦听器成为可能。但它不能帮助监听多个类型。我没有一个好的想法。instance of 不太美观。在我看来,一个更可容忍的想法是创建两个侦听器,一种用于每种类型。由于泛型擦除,它们必须是内部类,因为您的实际类无法同时监听多个类型。 - user949300
是的,我也考虑过两个类,但我希望有更优雅的解决方案。使用枚举(而不是接口)实现事件至少是可能的,但这样你可能最终会得到一个switch请参见我的答案)。此外,枚举还会施加其他潜在的限制。 - matsev
这似乎是一个可行的解决方案。但与Java的EventListenerList相比,它有哪些优势呢?您不喜欢它的原因是什么? - sou
显示剩余2条评论

1

采用老式的模式方法,使用访问者模式

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());
    }
}

更现代的方法是在事件类之间建立映射,这些事件类不应该相互派生,并且有各自的事件处理程序。这样可以避免访问者模式的缺点(即每次添加新事件时,您都需要更改所有访问者类,至少是它们的基类)。
另一种方法是使用组合模式
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();
    }
}

1

在经历了几乎每个人的建议的迭代之后,我最终采用了标准监听器接口和监听器列表的非常微小修改路线。我从Swing的EventListenerList开始,但对于数十种消息类型的添加/删除方法的数量感到失望。我意识到这不能被压缩,同时仍然保持单个EventListenerList,因此我开始考虑为每种类型单独创建一个列表。这使它类似于.NET事件,其中每个事件都持有其自己的委托列表,在引发时触发。我想避免大量的添加/删除方法,所以我制作了一个快速的Event类,它看起来像这样:

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;
    }
}

然后我会保留该类的几个实例,每个实例都根据监听器进行类型化,因此是 Event<MessageTypeAListener> 等等。我的类可以调用 add 方法将自己添加到特定事件中。我希望能够在 Event 实例上调用通用的 Raise 方法来触发所有处理程序,但我不希望它们都必须具有相同的 "handle" 方法,因此这是不可能的。因此,当我准备触发监听器时,我只需执行:
  for (MessageTypeAListener listener : messageTypeAEvent.getListeners())
      listener.onMessageTypeA(value);

我相信这不是一个新的想法,可能已经以更好/更健壮的方式完成了,但对我来说它很棒,我很满意。最重要的是,它很简单。

感谢所有的帮助。


0
如果你只有简单的事件,即没有数据的事件或者所有事件具有相同的数据类型,枚举可能是一个前进的方式。
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);
        }
    }
}

注释:

EventListnerImpl 中的 switch 语句可以省略,如果它只注册了一个事件,或者无论接收到哪个 Event,它都应该以相同的方式进行操作。

EventRegister 已经将 EventListener 存储在映射中,这意味着每个侦听器只会得到它已订阅的 Event 类型。此外,EventRegister 使用 Set,这意味着一个 EventListener 最多只会收到一次事件(以防止侦听器重复注册而导致其接收两个事件)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接