provider没有听众

    技术2024-03-24  90

    Swing框架以事件侦听器的形式广泛使用了Observer模式(也称为publish-subscribe模式)。 当用户与事件进行交互时,作为用户交互目标的Swing组件会触发事件; 数据更改时,数据模型类会触发事件。 以这种方式使用Observer可使控制器与模型分离,而模型与视图分离,从而简化了GUI应用程序的开发。

    “四人帮” 设计模式书(请参阅参考资料 )将观察者模式描述为定义“对象之间的一对多依赖关系,以便当一个对象改变状态时,其所有依赖关系都会被通知并自动更新”。 观察者模式可实现组件的松散耦合; 组件可以保持其状态同步,而不必直接了解彼此的身份或内部信息,从而促进了组件的重用。

    AWT和Swing组件(例如JButton或JTable )使用观察者模式将GUI事件的生成与其给定应用程序中的语义分离。 类似地,Swing模型类(例如TableModel和TreeModel )使用Observer将数据模型表示与视图生成分离,从而可以对同一数据使用多个独立视图。 Swing定义了Event和EventListener对象的层次结构; 可以生成事件的组件,例如JButton (可视组件)或TableModel (数据模型),提供了addXxxListener()和removeXxxListener()方法来注册和注销注册侦听器。 这些类决定何时需要触发事件,何时触发事件,它们调用所有已注册的侦听器。

    为了支持侦听器,一个对象需要维护一个已注册的侦听器列表,提供一种注册和注销侦听器的方法,并在发生适当的事件时调用每个侦听器。 侦听器易于使用和支持(不仅在GUI应用程序中),但应避免在注册界面的两侧都遇到一些陷阱:支持侦听器的组件和为其注册的组件。

    线程安全问题

    通常,侦听器是在与注册它们不同的线程中调用的。 为了支持从不同线程注册侦听器,用于存储和管理活动侦听器列表的任何机制必须是线程安全的。 Sun文档中的许多示例都使用Vector来存储侦听器列表,该列表可以解决部分问题,但不能解决整个问题。 触发事件时,触发该事件的组件将要迭代侦听器列表并调用每个侦听器,如果在迭代侦听器列表时某个线程碰巧想要添加或删除侦听器,则会带来并发修改的风险。

    管理侦听器列表

    假设您使用Vector<Listener>来存储您的侦听Vector<Listener>列表。 虽然Vector类是线程安全的,这意味着可以在不进行额外同步的情况下调用其方法,而不会破坏Vector数据结构,但迭代集合涉及“先检查后行动”序列,如果序列出错,则可能会失败。集合在迭代过程中被修改。 假设在迭代开始时列表中有三个侦听器。 在遍历Vector ,您反复调用size()和get()直到不再有要检索的元素为止,如清单1所示:

    清单1.向量的不安全迭代
    Vector<Listener> v; for (int i=0; i<v.size(); i++) v.get(i).eventHappened(event);

    但是,如果在您最后一次调用Vector.size()之后,有人从列表中删除了一个侦听器,会发生什么呢? 现在, Vector.get()将返回null (这是正确的,因为自从上次检查以来向量的状态已更改),并且当您尝试调用eventHappened()时将抛出NullPointerException 。 这是“先检查后行动”序列的一个示例-您检查是否还有其他元素,如果存在,则获取下一个元素-但是在存在并发修改的情况下,自检查。 图1说明了该问题:

    图1.并行迭代和修改,导致意外失败

    解决此问题的一种方法是在迭代期间将Vector保持在锁定状态。 另一种方法是克隆Vector或在每次事件发生时调用其toArray()方法以检索其内容。 这两种方法都会对性能产生影响。 第一个风险是在迭代期间锁定可能要访问侦听器列表的其他线程,第二个风险是创建一个临时对象并在每次事件发生时复制该列表。

    如果使用Iterator遍历侦听器列表,则相同的问题将以稍有不同的形式出现。 NullPointerException iterator()实现检测到自迭代开始以来已对集合进行了修改,则不会抛出NullPointerException ,而将抛出ConcurrentModificationException 。 同样,这可以通过在迭代期间锁定集合来防止。

    java.util.concurrent的CopyOnWriteArrayList类可以帮助防止此问题。 它实现List并且是线程安全的,但是其迭代器不会引发ConcurrentModificationException并且在遍历期间不需要任何其他锁定。 这种功能的组合是通过在每次修改列表时在内部重新分配和复制列表内容来实现的,因此迭代内容的线程不必处理更改-从它们的角度来看,列表内容在迭代过程中保持不变。 尽管这听起来效率低下,但请记住,在大多数“观察者”情况下,每个组件都有少量的侦听器,并且遍历会大大超过插入和删除的数量。 因此,更快的迭代弥补了较慢的变异,并提供了更好的并发性,因为多个线程可以同时迭代列表。

    初始化安全风险

    从其构造函数注册侦听器很诱人,但您应该避免这种诱惑。 它不仅使“失效的侦听器”问题(稍后讨论),而且还会引起一些线程安全性问题。 清单2展示了一种无害的尝试,它可以同时构造和注册一个侦听器。 问题在于,它在完全构造对象之前就使对对象的“ this”引用转义。 它看起来似乎无害,因为注册是构造函数所做的最后一件事,但看起来可能在欺骗:

    清单2.使“ this”引用转义的事件侦听器,引起麻烦
    public class EventListener { public EventListener(EventSource eventSource) { // do our initialization ... // register ourselves with the event source eventSource.registerListener(this); } public onEvent(Event e) { // handle the event } }

    子类化事件侦听器时,会出现这种方法的风险:现在,子类构造函数所做的任何事情都会在EventListener构造函数运行之后发生,因此在EventListener发布之后,会创建竞争条件。 有了一些不幸的时机,清单3中的onEvent方法可以在初始化列表字段之前被调用,从而在取消引用最终字段时引起非常混乱的NullPointerException :

    清单3.子类化清单2中的EventListener类引起的麻烦
    public class RecordingEventListener extends EventListener { private final ArrayList<Event> list; public RecordingEventListener(EventSource eventSource) { super(eventSource); list = Collections.synchronizedList(new ArrayList<Event>()); } public onEvent(Event e) { list.add(e); super.onEvent(e); } }

    即使您的侦听器类是最终的,因此不能进行子类化,您仍不应允许“ this”引用从构造函数中逸出-这样做会破坏Java内存模型提供的某些安全保证。 可以使“ this”引用转义而程序中不会出现单词“ this”。 发布一个非静态内部类实例具有相同的效果,因为内部类持有对其封闭对象的“ this”引用的引用。 意外允许“ this”引用转义的最常见原因之一是注册侦听器,如清单4所示。 不应从构造函数中注册事件侦听器!

    清单4.通过发布内部类实例隐式允许“ this”引用转义
    public class EventListener2 { public EventListener2(EventSource eventSource) { eventSource.registerListener( new EventListener() { public void onEvent(Event e) { eventReceived(e); } }); } public void eventReceived(Event e) { } }

    侦听器线程安全

    使用侦听器引起的第三个线程安全问题是由于侦听器可能要访问应用程序数据这一事实,并且通常在不直接在应用程序控制之下的线程中调用侦听器。 如果使用JButton或其他Swing组件注册了侦听器,则将从EDT中调用它。 侦听器代码可以安全地从EDT调用Swing组件上的方法,但是如果侦听器中的应用程序对象还不是线程安全的,则它们可能会对程序增加新的线程安全性要求。

    由于用户交互,Swing组件会生成事件,但是在调用fireXxxEvent()方法时,Swing模型类会生成事件。 这些方法将依次在被调用的线程中调用侦听器。 由于Swing模型类不是线程安全的,并且应该局限于EDT,因此对fireXxxEvent()任何调用也应从EDT执行。 如果要从另一个线程触发事件,则应使用Swing invokeLater()工具使方法在EDT中被调用。 通常,请注意将从何处调用线程事件侦听器,并确保它们所访问的任何对象在访问它们的任何地方都是线程安全的或通过适当的同步(或与Swing模型类相同的线程约束)进行保护的。

    听众不及格

    无论何时使用观察者模式,都将耦合两个独立的组件-观察者和被观察者,它们通常具有不同的生命周期。 注册侦听器的一个后果是,它会创建一个从观察到的对象到该侦听器的强引用-这可以防止在未注册该侦听器之前对侦听器(及其引用的任何对象)进行垃圾回收。 在许多情况下,侦听器的生命周期至少应与所观察到的组件一样长-许多侦听器会在整个应用程序持续时间内保持不变。 但是在某些情况下,原本打算短暂使用的侦听器最终将成为永久性侦听器,而唯一的证据表明它们的意外挥之不去是应用程序性能降低和内存使用量超出必要。

    “流失的侦听器”问题可能是由于在设计级别上的粗心而引起的:没有充分考虑所涉及对象的寿命,或者仅仅是通过草率的编码。 侦听器的注册和注销必须始终以配对的方式进行。 但是即使这样做,也必须确保注销实际上在正确的时间执行。 清单5给出了一个示例,说明惯用的编码成语存在失效的侦听器的风险。 它向组件注册一个侦听器,执行一些操作,然后注销该侦听器:

    清单5.监听器失效的代码
    public void processFile(String filename) throws IOException { cancelButton.registerListener(this); // open file, read it, process it // might throw IOException cancelButton.unregisterListener(this); }

    清单5的问题在于,如果文件处理代码抛出IOException-一种完全现实的可能性-侦听器永远不会被注销,这意味着它将永远不会被垃圾回收。 取消注册操作应在finally块中完成,以便它在processFile()方法之外的所有路径中执行。

    有时建议使用一种方法处理弱听者,即使用弱引用。 尽管这种方法是可行的,但是实现起来却非常棘手。 为了使它起作用,您需要找到另一个对象,该对象的生命周期恰好是侦听器的生命周期,并安排该对象持有对侦听器的强引用,但这并不总是那么容易。

    有时可以用来查找隐藏的失效的侦听器的另一种技术是防止给定的侦听器对象向给定的事件源注册两次。 这种情况通常表明存在一个错误-侦听器已注册但未取消注册,然后又被注册。 减轻这种影响而不必发现问题的一种方法是使用Set而不是List来存储侦听器。 或者,您可以在注册侦听器之前检查List是否已注册,如果存在则抛出异常(或记录错误),以便可以收集编码错误并采取措施。

    其他听众的过犯

    在编写侦听器时,您应该始终意识到执行侦听器的环境。 您不仅必须注意线程安全性问题,而且还需要记住,侦听器也可以通过其他方式为其调用者弄乱事情。 有一两件事,一个听者不应该做的是任何时间感知量的块; 它可能是从期望快速获得控制权的执行上下文中调用的。 如果侦听器将要执行可能耗时的操作,例如处理大型文档或执行可能会阻塞的操作(例如执行套接字IO),则应安排在另一个线程中进行该工作,以便可以快速返回到其调用方。

    侦听器可能会为麻烦的事件源带来麻烦的另一种方法是引发未经检查的异常。 虽然大多数时候我们从不打算抛出未经检查的异常,但有时还是会发生。 如果您使用清单1中的惯用语来调用侦听器,并且列表中的第二个侦听器抛出未检查的异常,则不仅不会调用后续侦听器(可能使应用程序处于不一致状态),而且甚至可能导致应用程序崩溃它在其中执行的线程,导致部分应用程序失败。

    当调用未知代码(侦听器肯定符合条件)时,谨慎地在try-catch块中执行它,以使行为不佳的侦听器不会造成不必要的损害。 您甚至可能想要自动取消注册引发未检查异常的侦听器。 毕竟,这是侦听器损坏的证据。 (您也可能希望将此日志记录下来,或者以其他方式引起用户的注意,以便用户可以弄清楚该程序为何无法按预期方式工作。)清单6显示了这种方法的示例,该方法将try-catch块嵌套在程序库中。迭代循环:

    清单6.健壮的侦听器调用
    List<Listener> list; for (Iterator<Listener> i=list.iterator; i.hasNext(); ) { Listener l = i.next(); try { l.eventHappened(event); } catch (RuntimeException e) { log("Unexpected exception in listener", e); i.remove(); } }

    结论

    观察者模式对于创建松散耦合的组件和鼓励组件重用非常有用,但是它具有侦听器编写者和组件编写者都应注意的一些风险。 注册侦听器时,请始终注意侦听器的生命周期。 如果要使用比应用程序更短的生存期,请确保未注册它,以便可以对其进行垃圾回收。 在编写侦听器和组件时,请注意所涉及的线程安全问题。 侦听器接触的任何对象都应该是线程安全的,或者对于线程受限的对象(例如Swing模型),侦听器应确信它在正确的线程中执行。


    翻译自: https://www.ibm.com/developerworks/java/library/j-jtp07265/index.html

    相关资源:微信小程序源码-合集6.rar
    Processed: 0.013, SQL: 9