【问题标题】:Is it possible to create my own event listener list in Java containing multiple listener types?是否可以在包含多种侦听器类型的 Java 中创建自己的事件侦听器列表?
【发布时间】:2011-11-22 18:13:14
【问题描述】:

我正在实现一个客户端-服务器系统,其中客户端处于连续阻塞读取循环中,侦听来自服务器的消息。当收到消息时,我想根据消息的类型引发一个“事件”,其他 GUI 类可能会添加监听器。我对 C# 事件比较熟悉,所以我仍然习惯 Java 的做事方式。

将有许多消息类型,因此我需要为每种类型提供一个接口,称为 MessageTypeAListener、MessageTypeBListener 等,每个都包含一个句柄方法,我的 GUI 类将实现这些方法。但是,将有许多类型,而不是维护每种类型的侦听器列表并具有多个“触发”方法,我希望拥有一个大侦听器列表和一个类型化的触发方法。然后 fire 方法可以说“只触发类型是我指定的侦听器”。

例如(伪代码):

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

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

...  

fire<MessageTypeAListener>(message);

但是,类型擦除似乎使这变得困难。我可以尝试强制转换和捕获异常,但这似乎是错误的。是否有一种干净的方式来实现这一点,或者为每种类型保留一个单独的侦听器列表是否更明智,即使会有大量类型?

【问题讨论】:

  • 查看EventListenerList - 它可以容纳任意数量的侦听器类型的侦听器。
  • 谢谢你——这门课似乎完全符合我的要求。这适用于该类本身不可用的 Android,但是我可以轻松获取源并将其包含在我的项目中。
  • 我对这个类如何工作很感兴趣。我检查了源代码,它为每个听众添加了两项到列表中。 item n 是监听器的 .class,item n+1 是监听器本身。然后当它想要获取某种类型的侦听器时传入 Class 并将其与列表中存储的 .class 进行比较。这样做是因为在运行时与侦听器的 .class 比较只会由于类型擦除而返回对象吗?但是将 .class 本身存储在列表中会保留它吗?
  • 不完全,这与类型擦除没有任何关系。事实上,.class 引用是静态常量,因此在编译时是已知的。它只是使用类作为监听器映射的键,允许它为任何类保存监听器。它在概念上是一个Map&lt;Class, List&gt;,但使用一个类-监听器对数组来实现。
  • 好的,有道理。但是如果你只存储监听器,然后在你的循环中(假设你传入一个 Class 的“t”)做 if (listener.class== t) 呢?而不是 list[某个静态类常量的索引] == t?在循环中调用 listener.class 会不会返回相同的东西?

标签: java addeventlistener event-listener type-erasure


【解决方案1】:

我实现了类似的东西,因为我发自内心地不喜欢 Java 的 EventListenerList。首先,您实现一个通用的侦听器。我根据接收到的事件定义了监听器,基本上只有一种方法

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

这使您不必定义 ListenerA、ListernerB 等...虽然您可以使用 ListenerA、ListenerB 等按照自己的方式进行,所有这些都扩展了一些基础,例如 MyListener。两种方式各有利弊。

然后我使用CopyOnWriteArraySet 来容纳所有这些听众。一个集合是需要考虑的,因为经常听者被草率的编码人员添加两次。 YMMV。但是,实际上你有一个Collection&lt;GenericListener&lt;T extends Event&gt;&gt; or a Collection&lt;MyListener&gt;

现在,正如您所发现的,通过类型擦除,集合只能容纳一种类型的侦听器。这通常是一个问题。解决方案:使用地图。

由于我的一切都基于事件,所以我使用了

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

根据事件的类别,获取想要获取该事件的侦听器列表。
您的替代方法是将其基于侦听器的类

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

上面可能有一些错别字...

【讨论】:

  • P.S.你不需要说 T extends Event。在这种情况下,您可以广播事件以外的对象,例如异常、数据等。
  • 假设您有FirstEventSecondEvent(都扩展了Event)。如果您想同时听两者,您是否必须退回到MyListener implements GenericListener&lt;Event&gt;,然后做一个丑陋的instanceof,还是有更漂亮的解决方案?显然,public class MyListener implements GenericListener&lt;FirstEvent&gt;, GenericListener&lt;SecondEvent&gt; 不起作用:-(
  • Map 可以广播多种事件类型并为每种类型设置监听器。 listening 对多种类型没有帮助。我对此没有一个的想法。的实例并不漂亮。一个更容易接受的想法,IMO,是创建两个侦听器,每种类型一次。它们必须在内部类中,因为您的真实类由于擦除而无法收听多种类型。
  • 是的,我也想到了两个类,但我希望有更优雅的东西。使用enum(相对于interface)事件实现,至少是可能的,但您最终可能会得到switchsee my answer)。此外,enum 还会施加其他潜在的限制。
  • 枚举是一个有趣的想法 - 在某些有限的情况下会很好地工作。
【解决方案2】:

老式模式方法,使用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());
    }
}

更现代的方法是在不应该相互派生的事件类和这些事件的各自处理程序之间使用 Map。这样可以避免访问者模式的缺点(也就是说,每次添加新事件时,您都需要更改所有访问者类,至少是它们的基类)。

另外一种方式是使用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】:

    在经历了这里几乎每个人的建议的迭代之后,我最终采用了标准监听器接口和监听器列表的略微修改的路线。我从 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&lt;MessageTypeAListener&gt; 等。然后我的类可以调用 add 方法将自己添加到该特定事件中。我希望能够在Event 实例上调用通用Raise 方法,然后触发所有处理程序,但我不希望它们都必须具有相同的“处理”方法,所以这是不可能。相反,当我准备解雇听众时,我就这么做了

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

    我确信这不是一个新想法,并且可能以前已经以更好/更强大的方式完成了,但它对我来说非常有用,我对此很满意。最重要的是,它很简单。

    感谢大家的帮助。

    【讨论】:

      【解决方案4】:

      如果您只有简单的事件,即没有数据的事件或所有事件都具有相同的数据类型,enum 可能是一个前进的方向:

      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(s) 存储在映射中,这意味着每个侦听器只会获得其订阅的Event(s)。此外,EventRegister 使用Sets,这意味着EventListener 最多只能接收一次事件(以防止如果有人不小心注册了两次侦听器,侦听器将收到两个事件)。

      【讨论】:

        猜你喜欢
        • 2014-01-20
        • 1970-01-01
        • 2018-08-28
        • 1970-01-01
        • 1970-01-01
        • 2019-12-22
        • 1970-01-01
        • 1970-01-01
        • 2011-10-11
        相关资源
        最近更新 更多