<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Vincent

    Vicent's blog
    隨筆 - 74, 文章 - 0, 評論 - 5, 引用 - 0
    數據加載中……

    Java 理論與實踐: 做個好的(事件)偵聽器

    觀察者模式在 Swing 開發中很常見,在 GUI 應用程序以外的場景中,它對于消除組件的耦合性也非常有用。但是,仍然存在一些偵聽器登記和調用方面的常見缺陷。在 Java 理論與實踐 的這一期中,Java 專家 Brian Goetz 就如何做一個好的偵聽器,以及如何對您的偵聽器也友好,提供了一些感覺很好的建議。請在相應的 討論論壇 上與作者和其他讀者分享您對這篇文章的想法。(您也可以單擊本文頂部或底部的 討論 訪問論壇。)

    Swing 框架以事件偵聽器的形式廣泛利用了觀察者模式(也稱為發布-訂閱模式)。Swing 組件作為用戶交互的目標,在用戶與它們交互的時候觸發事件;數據模型類在數據發生變化時觸發事件。用這種方式使用觀察者,可以讓控制器與模型分離,讓模型與視圖分離,從而簡化 GUI 應用程序的開發。

    “四人幫”的 設計模式 一書(參閱 參考資料)把觀察者模式描述為:定義對象之間的“一對多”關系,這樣一個對象改變狀態時,所有它的依賴項都會被通知,并自動更新。觀察者模式支持組件之間的松散耦合;組件可以保持它們的狀態同步,卻不需要直接知道彼此的標識或內部情況,從而促進了組件的重用。

    AWT 和 Swing 組件(例如 JButtonJTable)使用觀察者模式消除了 GUI 事件生成與它們在指定應用程序中的語義之間的耦合。類似地,Swing 的模型類,例如 TableModelTreeModel,也使用觀察者消除數據模型表示 與視圖生成之間的耦合,從而支持相同數據的多個獨立的視圖。Swing 定義了 EventEventListener 對象層次結構;可以生成事件的組件,例如 JButton(可視組件) 或 TableModel(數據模型),提供了 addXxxListener()removeXxxListener() 方法,用于偵聽器的登記和取消登記。這些類負責決定什么時候它們需要觸發事件,什么時候確實觸發事件,以及什么時候調用所有登記的偵聽器。

    為了支持偵聽器,對象需要維護一個已登記的偵聽器列表,提供偵聽器登記和取消登記的手段,并在適當的事件發生時調用每個偵聽器。使用和支持偵聽器很容易(不僅僅在 GUI 應用程序中),但是在登記接口的兩邊(它們是支持偵聽器的組件和登記偵聽器的組件)都應當避免一些缺陷。

    線程安全問題

    通常,調用偵聽器的線程與登記偵聽器的線程不同。要支持從不同線程登記偵聽器,那么不管用什么機制存儲和管理活動偵聽器列表,這個機制都必須是線程安全的。Sun 的文檔中的許多示例使用 Vector 保存偵聽器列表,它解決了部分問題,但是沒有解決全部問題。在事件觸發時,觸發它的組件會考慮迭代偵聽器列表,并調用每個偵聽器,這就帶來了并發修改的風險,比如在偵聽器列表迭代期間,某個線程偶然想添加或刪除一個偵聽器。

    管理偵聽器列表

    假設您使用 Vector<Listener> 保存偵聽器列表。雖然 Vector 類是線程安全的(意味著不需要進行額外的同步就可調用它的方法,沒有破壞 Vector 數據結構的風險),但是集合的迭代中包含“檢測然后執行”序列,如果在迭代期間集合被修改,就有了失敗的風險。假設迭代開始時列表中有三個偵聽器。在迭代 Vector 時,重復調用 size()get() 方法,直到所有元素都檢索完,如清單 1 所示:


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

    但是,如果恰好就在最后一次調用 Vector.size() 之后,有人從列表中刪除了一個偵聽器,會發生什么呢?現在,Vector.get() 將返回 null (這是對的,因為從上次檢測 vector 的狀態以來,它的狀態已經變了),而在試圖調用 eventHappened() 時,會拋出 NullPointerException。這是“檢測然后執行”序列的一個示例 —— 檢測是否存在更多元素,如果存在,就取得下一元素 —— 但是在存在并發修改的情況下,檢測之后狀態可能已經變化。圖 1 演示了這個問題:

    圖 1. 并發迭代和修改,造成意料之外的失敗

    并發迭代和修改,造成意料之外的失敗

    這個問題的一個解決方案是在迭代期間持有對 Vector 的鎖;另一個方案是克隆 Vector 或調用它的 toArray() 方法,在每次發生事件時檢索它的內容。所有這兩個方法都有性能上的問題:第一個的風險是在迭代期間,會把其他想訪問偵聽器列表的線程鎖在外面;第二個則要創建臨時對象,而且每次事件發生時都要拷貝列表。

    如果用迭代器(Iterator)去遍歷偵聽器列表,也會有同樣的問題,只是表現略有不同; iterator() 實現不拋出 NullPointerException,它在探測到迭代開始之后集合發生修改時,會拋出 ConcurrentModificationException。同樣,也可以通過在迭代期間鎖定集合防止這個問題。

    java.util.concurrent 中的 CopyOnWriteArrayList 類,能夠幫助防止這個問題。它實現了 List,而且是線程安全的,但是它的迭代器不會拋出 ConcurrentModificationException,遍歷期間也不要求額外的鎖定。這種特性組合是通過在每次列表修改時,在內部重新分配并拷貝列表內容而實現的,這樣,遍歷內容的線程不需要處理變化 —— 從它們的角度來說,列表的內容在遍歷期間保持不變。雖然這聽起來可能沒效率,但是請記住,在多數觀察者情況下,每個組件只有少量偵聽器,遍歷的數量遠遠超過插入和刪除的數量。所以更快的迭代可以補償較慢的變化過程,并提供更好的并發性,因為多個線程可以同時迭代列表。

    初始化的安全風險

    從偵聽器的構造函數中登記它很誘惑人,但是這是一個應當避免的誘惑。它僅會造成“失效偵聽器(lapsed listener)的問題(我稍后討論它),而且還會造成多個線程安全問題。清單 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 方法會在列表字段還沒初始化之前就被調用,從而在取消 final 字段的引用時,會生成非常讓人困惑的 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);
      }
    }
    
    												
    										

    即使偵聽器類是 final 的,不能派生子類,也不應當允許“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 組件生成的事件是用戶交互的結果,但是 Swing 模型類是在 fireXxxEvent() 方法被調用的時候生成事件。這些方法又會在調用它們的線程中調用偵聽器。因為 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() 方法的所有出口都會執行它。

    有時推薦的一個處理失效偵聽器的方法是使用弱引用。雖然這種方法可行,但是實現起來很麻煩。要讓它工作,需要找到另外一個對象,它的生命周期恰好是偵聽器的生命周期,并安排它持有對偵聽器的強引用,這可不是件容易的事。

    另外一項可以用來找到隱藏失效偵聽器的技術是:防止指定偵聽器對象在指定事件源上登記兩次。這種情況通常是 bug 的跡象 —— 偵聽器登記了,但是沒有取消登記,然后再次登記。不用檢測問題,就能緩解這個問題的影響的一種方式是:使用 Set 代替 List 來存儲偵聽器;或者也可以檢測 List,在登記偵聽器之前檢查是否已經登記了,如果已經登記,就拋出異常(或記錄錯誤),這樣就可以搜集編碼錯誤的證據,并采取行動。





    回頁首


    其他偵聽器問題

    在編寫偵聽器時,應當一直注意它們將要執行的環境。不僅要注意線程安全問題,還需要記住:偵聽器也可以用其他方式為它的調用者把事情搞糟。偵聽器 不該 做的一件事是:阻塞相當長一段時間(長得可以感覺得到);調用它的執行上下文很可能希望迅速返回控制。如果偵聽器要執行一個可能比較費時的操作,例如處理大型文本,或者要做的工作可能阻塞,例如執行 socket 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 模型),偵聽器應當確定自己正在正確的線程中執行。

    posted on 2006-08-24 17:43 Binary 閱讀(231) 評論(0)  編輯  收藏 所屬分類: j2se

    主站蜘蛛池模板: 自拍偷自拍亚洲精品偷一| 亚洲精品人成网线在线播放va| 亚洲AV无码一区二区三区牛牛| 人人揉揉香蕉大免费不卡| 你懂的免费在线观看| 久久精品国产免费观看| 噜噜噜亚洲色成人网站∨| 国产va在线观看免费| 亚洲av无码潮喷在线观看| 亚洲av中文无码字幕色不卡| 国产又黄又爽又猛的免费视频播放 | 亚洲成av人在线视| a在线视频免费观看| 久久综合日韩亚洲精品色| 男的把j放进女人下面视频免费| 亚洲精品无码久久久久sm| 亚洲熟妇少妇任你躁在线观看| 三年在线观看免费观看完整版中文 | 破了亲妺妺的处免费视频国产| 亚洲国产无线乱码在线观看| 国产乱子伦片免费观看中字| 免费无码午夜福利片| aⅴ在线免费观看| 国产亚洲精品影视在线| www亚洲一级视频com| 激情五月亚洲色图| 99爱视频99爱在线观看免费| 亚洲二区在线视频| 久久国产免费一区| 久久亚洲精品国产亚洲老地址| 亚洲精品无码av天堂| 久久中文字幕免费视频| 亚洲精品无码不卡在线播放HE| 99精品在线免费观看| 亚洲国产精品自在自线观看| 亚洲片国产一区一级在线观看| 久久久久久久99精品免费| 亚洲一区二区无码偷拍| 亚洲熟妇无码乱子AV电影| 高潮毛片无遮挡高清免费| 在线看片无码永久免费aⅴ |