Vincent

          Vicent's blog
          隨筆 - 74, 文章 - 0, 評(píng)論 - 5, 引用 - 0
          數(shù)據(jù)加載中……

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

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

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

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

          AWT 和 Swing 組件(例如 JButtonJTable)使用觀察者模式消除了 GUI 事件生成與它們?cè)谥付☉?yīng)用程序中的語(yǔ)義之間的耦合。類似地,Swing 的模型類,例如 TableModelTreeModel,也使用觀察者消除數(shù)據(jù)模型表示 與視圖生成之間的耦合,從而支持相同數(shù)據(jù)的多個(gè)獨(dú)立的視圖。Swing 定義了 EventEventListener 對(duì)象層次結(jié)構(gòu);可以生成事件的組件,例如 JButton(可視組件) 或 TableModel(數(shù)據(jù)模型),提供了 addXxxListener()removeXxxListener() 方法,用于偵聽器的登記和取消登記。這些類負(fù)責(zé)決定什么時(shí)候它們需要觸發(fā)事件,什么時(shí)候確實(shí)觸發(fā)事件,以及什么時(shí)候調(diào)用所有登記的偵聽器。

          為了支持偵聽器,對(duì)象需要維護(hù)一個(gè)已登記的偵聽器列表,提供偵聽器登記和取消登記的手段,并在適當(dāng)?shù)氖录l(fā)生時(shí)調(diào)用每個(gè)偵聽器。使用和支持偵聽器很容易(不僅僅在 GUI 應(yīng)用程序中),但是在登記接口的兩邊(它們是支持偵聽器的組件和登記偵聽器的組件)都應(yīng)當(dāng)避免一些缺陷。

          線程安全問(wèn)題

          通常,調(diào)用偵聽器的線程與登記偵聽器的線程不同。要支持從不同線程登記偵聽器,那么不管用什么機(jī)制存儲(chǔ)和管理活動(dòng)偵聽器列表,這個(gè)機(jī)制都必須是線程安全的。Sun 的文檔中的許多示例使用 Vector 保存?zhèn)陕犉髁斜?,它解決了部分問(wèn)題,但是沒(méi)有解決全部問(wèn)題。在事件觸發(fā)時(shí),觸發(fā)它的組件會(huì)考慮迭代偵聽器列表,并調(diào)用每個(gè)偵聽器,這就帶來(lái)了并發(fā)修改的風(fēng)險(xiǎn),比如在偵聽器列表迭代期間,某個(gè)線程偶然想添加或刪除一個(gè)偵聽器。

          管理偵聽器列表

          假設(shè)您使用 Vector<Listener> 保存?zhèn)陕犉髁斜怼km然 Vector 類是線程安全的(意味著不需要進(jìn)行額外的同步就可調(diào)用它的方法,沒(méi)有破壞 Vector 數(shù)據(jù)結(jié)構(gòu)的風(fēng)險(xiǎn)),但是集合的迭代中包含“檢測(cè)然后執(zhí)行”序列,如果在迭代期間集合被修改,就有了失敗的風(fēng)險(xiǎn)。假設(shè)迭代開始時(shí)列表中有三個(gè)偵聽器。在迭代 Vector 時(shí),重復(fù)調(diào)用 size()get() 方法,直到所有元素都檢索完,如清單 1 所示:


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

          但是,如果恰好就在最后一次調(diào)用 Vector.size() 之后,有人從列表中刪除了一個(gè)偵聽器,會(huì)發(fā)生什么呢?現(xiàn)在,Vector.get() 將返回 null (這是對(duì)的,因?yàn)閺纳洗螜z測(cè) vector 的狀態(tài)以來(lái),它的狀態(tài)已經(jīng)變了),而在試圖調(diào)用 eventHappened() 時(shí),會(huì)拋出 NullPointerException。這是“檢測(cè)然后執(zhí)行”序列的一個(gè)示例 —— 檢測(cè)是否存在更多元素,如果存在,就取得下一元素 —— 但是在存在并發(fā)修改的情況下,檢測(cè)之后狀態(tài)可能已經(jīng)變化。圖 1 演示了這個(gè)問(wèn)題:

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

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

          這個(gè)問(wèn)題的一個(gè)解決方案是在迭代期間持有對(duì) Vector 的鎖;另一個(gè)方案是克隆 Vector 或調(diào)用它的 toArray() 方法,在每次發(fā)生事件時(shí)檢索它的內(nèi)容。所有這兩個(gè)方法都有性能上的問(wèn)題:第一個(gè)的風(fēng)險(xiǎn)是在迭代期間,會(huì)把其他想訪問(wèn)偵聽器列表的線程鎖在外面;第二個(gè)則要?jiǎng)?chuàng)建臨時(shí)對(duì)象,而且每次事件發(fā)生時(shí)都要拷貝列表。

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

          java.util.concurrent 中的 CopyOnWriteArrayList 類,能夠幫助防止這個(gè)問(wèn)題。它實(shí)現(xiàn)了 List,而且是線程安全的,但是它的迭代器不會(huì)拋出 ConcurrentModificationException,遍歷期間也不要求額外的鎖定。這種特性組合是通過(guò)在每次列表修改時(shí),在內(nèi)部重新分配并拷貝列表內(nèi)容而實(shí)現(xiàn)的,這樣,遍歷內(nèi)容的線程不需要處理變化 —— 從它們的角度來(lái)說(shuō),列表的內(nèi)容在遍歷期間保持不變。雖然這聽起來(lái)可能沒(méi)效率,但是請(qǐng)記住,在多數(shù)觀察者情況下,每個(gè)組件只有少量偵聽器,遍歷的數(shù)量遠(yuǎn)遠(yuǎn)超過(guò)插入和刪除的數(shù)量。所以更快的迭代可以補(bǔ)償較慢的變化過(guò)程,并提供更好的并發(fā)性,因?yàn)槎鄠€(gè)線程可以同時(shí)迭代列表。

          初始化的安全風(fēng)險(xiǎn)

          從偵聽器的構(gòu)造函數(shù)中登記它很誘惑人,但是這是一個(gè)應(yīng)當(dāng)避免的誘惑。它僅會(huì)造成“失效偵聽器(lapsed listener)的問(wèn)題(我稍后討論它),而且還會(huì)造成多個(gè)線程安全問(wèn)題。清單 2 顯示了一個(gè)看起來(lái)沒(méi)什么害處的同時(shí)構(gòu)造和登記偵聽器的企圖。問(wèn)題是:它造成到對(duì)象的“this”引用在對(duì)象完全構(gòu)造完成之前轉(zhuǎn)義。雖然看起來(lái)沒(méi)什么害處,因?yàn)榈怯浭菢?gòu)造函數(shù)做的最后一件事,但是看到的東西是有欺騙性的:


          清單 2. 事件偵聽器允許“this”引用轉(zhuǎn)義,造成問(wèn)題
          												
          														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
            }
          }
          
          												
          										

          在繼承事件偵聽器的時(shí)候,會(huì)出現(xiàn)這種方法的一個(gè)風(fēng)險(xiǎn):這時(shí),子類構(gòu)造函數(shù)做的任何工作都是在 EventListener 構(gòu)造函數(shù)運(yùn)行之后進(jìn)行的,也就是在 EventListener 發(fā)布之后,所以會(huì)造成爭(zhēng)用情況。在某些不幸的時(shí)候,清單 3 中的 onEvent 方法會(huì)在列表字段還沒(méi)初始化之前就被調(diào)用,從而在取消 final 字段的引用時(shí),會(huì)生成非常讓人困惑的 NullPointerException 異常:


          清單 3. 繼承清單 2 的 EventListener 類造成的問(wèn)題
          												
          														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 的,不能派生子類,也不應(yīng)當(dāng)允許“this”引用在構(gòu)造函數(shù)中轉(zhuǎn)義 —— 這樣做會(huì)危害 Java 內(nèi)存模型的某些安全保證。如果“this”這個(gè)詞不會(huì)出現(xiàn)在程序中,就可讓“this”引用轉(zhuǎn)義;發(fā)布一個(gè)非靜態(tài)內(nèi)部類實(shí)例可以達(dá)到相同的效果,因?yàn)閮?nèi)部類持有對(duì)它包圍的對(duì)象的“this”引用的引用。偶然地允許“this”引用轉(zhuǎn)義的最常見原因,就是登記偵聽器,如清單 4 所示。事件偵聽器不應(yīng)當(dāng)在構(gòu)造函數(shù)中登記!


          清單 4. 通過(guò)發(fā)布內(nèi)部類實(shí)例,顯式地允許“this”引用轉(zhuǎn)義
          												
          														public class EventListener2 {
            public EventListener2(EventSource eventSource) {
          
              eventSource.registerListener(
                new EventListener() {
                  public void onEvent(Event e) { 
                    eventReceived(e);
                  }
                });
            }
          
            public void eventReceived(Event e) {
            }
          }
          
          												
          										

          偵聽器線程安全

          使用偵聽器造成的第三個(gè)線程安全問(wèn)題來(lái)自這個(gè)事實(shí):偵聽器可能想訪問(wèn)應(yīng)用程序數(shù)據(jù),而調(diào)用偵聽器的線程通常不直接在應(yīng)用程序的控制之下。如果在 JButton 或其他 Swing 組件上登記偵聽器,那么會(huì)從 EDT 調(diào)用該偵聽器。偵聽器的代碼可以從 EDT 安全地調(diào)用 Swing 組件上的方法,但是如果對(duì)象本身不是線程安全的,那么從偵聽器訪問(wèn)應(yīng)用程序?qū)ο髸?huì)給應(yīng)用程序增加新的線程安全需求。

          Swing 組件生成的事件是用戶交互的結(jié)果,但是 Swing 模型類是在 fireXxxEvent() 方法被調(diào)用的時(shí)候生成事件。這些方法又會(huì)在調(diào)用它們的線程中調(diào)用偵聽器。因?yàn)?Swing 模型類不是線程安全的,而且假設(shè)被限制在 EDT 內(nèi),所以對(duì) fireXxxEvent() 的任何調(diào)用也都應(yīng)當(dāng)從 EDT 執(zhí)行。如果想從另外的線程觸發(fā)事件,那么應(yīng)當(dāng)用 Swing 的 invokeLater() 功能讓方法轉(zhuǎn)而在 EDT 內(nèi)調(diào)用。一般來(lái)說(shuō),要注意調(diào)用事件偵聽器的線程,還要保證它們涉及的任何對(duì)象或者是線程安全的,或者在訪問(wèn)它們的地方,受到適當(dāng)?shù)耐剑ɑ蛘呤?Swing 模型類的線程約束)的保護(hù)。





          回頁(yè)首


          失效偵聽器

          不管什么時(shí)候使用觀察者模式,都耦合著兩個(gè)獨(dú)立組件 —— 觀察者和被觀察者,它們通常有不同的生命周期。登記偵聽器的后果之一就是:它在被觀察對(duì)象和偵聽器之間建立起很強(qiáng)的引用關(guān)系,這種關(guān)系防止偵聽器(以及它引用的對(duì)象)被垃圾收集,直到偵聽器取消登記為止。在許多情況下,偵聽器的生命周期至少要和被觀察的組件一樣長(zhǎng) —— 許多偵聽器會(huì)在整個(gè)應(yīng)用程序期間都存在。但是在某些情況下,應(yīng)當(dāng)短期存在的偵聽器最后變成了永久的,它們這種無(wú)意識(shí)的拖延的證據(jù)就是應(yīng)用程序性能變慢、高于必需的內(nèi)存使用。

          “失效偵聽器”的問(wèn)題可以由設(shè)計(jì)級(jí)別上的不小心造成:沒(méi)有恰當(dāng)?shù)乜紤]包含的對(duì)象的壽命,或者由于松懈的編碼。偵聽器登記和取消登記應(yīng)當(dāng)結(jié)對(duì)進(jìn)行。但是即使這么做,也必須保證是在正確的時(shí)間執(zhí)行取消登記。清單 5 顯示了會(huì)造成失效偵聽器的編碼習(xí)慣的示例。它在組件上登記偵聽器,執(zhí)行某些動(dòng)作,然后取消登記偵聽器:


          清單 5. 有造成失效偵聽器風(fēng)險(xiǎn)的代碼
          												
          														  public void processFile(String filename) throws IOException {
              cancelButton.registerListener(this);
              // open file, read it, process it
              // might throw IOException
              cancelButton.unregisterListener(this);
            }
          
          												
          										

          清單 5 的問(wèn)題是:如果文件處理代碼拋出了 IOException —— 這是很有可能的 —— 那么偵聽器就永遠(yuǎn)不會(huì)取消登記,這就意味著它永遠(yuǎn)不會(huì)被垃圾收集。取消登記的操作應(yīng)當(dāng)在 finally 塊中進(jìn)行,這樣,processFile() 方法的所有出口都會(huì)執(zhí)行它。

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

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





          回頁(yè)首


          其他偵聽器問(wèn)題

          在編寫偵聽器時(shí),應(yīng)當(dāng)一直注意它們將要執(zhí)行的環(huán)境。不僅要注意線程安全問(wèn)題,還需要記?。簜陕犉饕部梢杂闷渌绞綖樗恼{(diào)用者把事情搞糟。偵聽器 不該 做的一件事是:阻塞相當(dāng)長(zhǎng)一段時(shí)間(長(zhǎng)得可以感覺得到);調(diào)用它的執(zhí)行上下文很可能希望迅速返回控制。如果偵聽器要執(zhí)行一個(gè)可能比較費(fèi)時(shí)的操作,例如處理大型文本,或者要做的工作可能阻塞,例如執(zhí)行 socket IO,那么偵聽器應(yīng)當(dāng)把這些操作安排在另一個(gè)線程中進(jìn)行,這樣它就可以迅速返回它的調(diào)用者。

          對(duì)于不小心的事件源,偵聽器會(huì)造成麻煩的另一個(gè)方式是:拋出未檢測(cè)的異常。雖然大多數(shù)時(shí)候,我們不會(huì)故意拋出未檢測(cè)異常,但是確實(shí)有些時(shí)候會(huì)發(fā)生這種情況。如果使用清單 1 的方式調(diào)用偵聽器,列表中的第二個(gè)偵聽器就會(huì)拋出未檢測(cè)異常,那么不僅后續(xù)的偵聽器得不到調(diào)用(可能造成應(yīng)用程序處在不一致的狀態(tài)),而且有可能把執(zhí)行它的線程破壞掉,從而造成局部應(yīng)用程序失敗。

          在調(diào)用未知代碼(偵聽器就是這樣的代碼)時(shí),謹(jǐn)慎的方式是在 try-catch 塊中執(zhí)行它,這樣,行為有誤的偵聽器不會(huì)造成更多不必要的破壞。對(duì)于拋出未檢測(cè)異常的偵聽器,您可能想自動(dòng)對(duì)它取消登記,畢竟,拋出未檢測(cè)異常就證明偵聽器壞掉了。(您可能還想記錄這個(gè)錯(cuò)誤或者提醒用戶注意,好讓用戶能夠知道為什么程序停止像期望的那樣繼續(xù)工作。)清單 6 顯示了這種方式的一個(gè)示例,它在迭代循環(huán)內(nèi)部嵌套了 try-catch 塊:


          清單 6. 健壯的偵聽器調(diào)用
          												
          														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();
              }
          }
          
          												
          										





          回頁(yè)首


          結(jié)束語(yǔ)

          觀察者模式對(duì)于創(chuàng)建松散耦合的組件、鼓勵(lì)組件重用非常有用,但是它有一些風(fēng)險(xiǎn),偵聽器的編寫者和組件的編寫者都應(yīng)當(dāng)注意。在登記偵聽器時(shí),應(yīng)當(dāng)一直注意偵聽器的生命周期。如果偵聽器的壽命應(yīng)當(dāng)比應(yīng)用程序的短,那么請(qǐng)確保取消它的登記,這樣它就可以被垃圾收集。在編寫偵聽器和組件時(shí),請(qǐng)注意它包含的線程安全性問(wèn)題。偵聽器涉及的任何對(duì)象,都應(yīng)當(dāng)是線程安全的,或者是受線程約束的對(duì)象(例如 Swing 模型),偵聽器應(yīng)當(dāng)確定自己正在正確的線程中執(zhí)行。

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

          主站蜘蛛池模板: 宜兰县| 大厂| 米脂县| 上栗县| 井冈山市| 酒泉市| 麻城市| 洪泽县| 威信县| 洱源县| 定兴县| 遂川县| 遂溪县| 桂平市| 汝南县| 尉犁县| 雅江县| 绥江县| 长治市| 乌兰察布市| 宝清县| 兴和县| 军事| 天津市| 修文县| 惠来县| 怀柔区| 溆浦县| 承德县| 秭归县| 五寨县| 宜昌市| 蓝田县| 南开区| 镇江市| 黎川县| 九龙坡区| 昌黎县| 太原市| 来安县| 漯河市|