<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, 評(píng)論 - 5, 引用 - 0
    數(shù)據(jù)加載中……

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

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

    Swing 框架以事件偵聽器的形式廣泛利用了觀察者模式(也稱為發(fā)布-訂閱模式)。Swing 組件作為用戶交互的目標(biāo),在用戶與它們交互的時(shí)候觸發(fā)事件;數(shù)據(jù)模型類在數(shù)據(jù)發(fā)生變化時(shí)觸發(fā)事件。用這種方式使用觀察者,可以讓控制器與模型分離,讓模型與視圖分離,從而簡化 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 事件生成與它們在指定應(yīng)用程序中的語義之間的耦合。類似地,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)避免一些缺陷。

    線程安全問題

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

    管理偵聽器列表

    假設(shè)您使用 Vector<Listener> 保存?zhèn)陕犉髁斜怼km然 Vector 類是線程安全的(意味著不需要進(jìn)行額外的同步就可調(diào)用它的方法,沒有破壞 Vector 數(shù)據(jù)結(jié)構(gòu)的風(fēng)險(xiǎn)),但是集合的迭代中包含“檢測然后執(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測 vector 的狀態(tài)以來,它的狀態(tài)已經(jīng)變了),而在試圖調(diào)用 eventHappened() 時(shí),會(huì)拋出 NullPointerException。這是“檢測然后執(zhí)行”序列的一個(gè)示例 —— 檢測是否存在更多元素,如果存在,就取得下一元素 —— 但是在存在并發(fā)修改的情況下,檢測之后狀態(tài)可能已經(jīng)變化。圖 1 演示了這個(gè)問題:

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

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

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

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

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

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

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


    清單 2. 事件偵聽器允許“this”引用轉(zhuǎ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ì)造成爭用情況。在某些不幸的時(shí)候,清單 3 中的 onEvent 方法會(huì)在列表字段還沒初始化之前就被調(diào)用,從而在取消 final 字段的引用時(shí),會(huì)生成非常讓人困惑的 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 的,不能派生子類,也不應(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. 通過發(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è)線程安全問題來自這個(gè)事實(shí):偵聽器可能想訪問應(yīng)用程序數(shù)據(jù),而調(diào)用偵聽器的線程通常不直接在應(yīng)用程序的控制之下。如果在 JButton 或其他 Swing 組件上登記偵聽器,那么會(huì)從 EDT 調(diào)用該偵聽器。偵聽器的代碼可以從 EDT 安全地調(diào)用 Swing 組件上的方法,但是如果對(duì)象本身不是線程安全的,那么從偵聽器訪問應(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)用。一般來說,要注意調(diào)用事件偵聽器的線程,還要保證它們涉及的任何對(duì)象或者是線程安全的,或者在訪問它們的地方,受到適當(dāng)?shù)耐剑ɑ蛘呤?Swing 模型類的線程約束)的保護(hù)。





    回頁首


    失效偵聽器

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

    “失效偵聽器”的問題可以由設(shè)計(jì)級(jí)別上的不小心造成:沒有恰當(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 的問題是:如果文件處理代碼拋出了 IOException —— 這是很有可能的 —— 那么偵聽器就永遠(yuǎn)不會(huì)取消登記,這就意味著它永遠(yuǎn)不會(huì)被垃圾收集。取消登記的操作應(yīng)當(dāng)在 finally 塊中進(jìn)行,這樣,processFile() 方法的所有出口都會(huì)執(zhí)行它。

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

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





    回頁首


    其他偵聽器問題

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

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

    在調(diào)用未知代碼(偵聽器就是這樣的代碼)時(shí),謹(jǐn)慎的方式是在 try-catch 塊中執(zhí)行它,這樣,行為有誤的偵聽器不會(huì)造成更多不必要的破壞。對(duì)于拋出未檢測異常的偵聽器,您可能想自動(dòng)對(duì)它取消登記,畢竟,拋出未檢測異常就證明偵聽器壞掉了。(您可能還想記錄這個(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();
        }
    }
    
    												
    										





    回頁首


    結(jié)束語

    觀察者模式對(duì)于創(chuàng)建松散耦合的組件、鼓勵(lì)組件重用非常有用,但是它有一些風(fēng)險(xiǎn),偵聽器的編寫者和組件的編寫者都應(yīng)當(dāng)注意。在登記偵聽器時(shí),應(yīng)當(dāng)一直注意偵聽器的生命周期。如果偵聽器的壽命應(yīng)當(dāng)比應(yīng)用程序的短,那么請確保取消它的登記,這樣它就可以被垃圾收集。在編寫偵聽器和組件時(shí),請注意它包含的線程安全性問題。偵聽器涉及的任何對(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

    主站蜘蛛池模板: 中文字幕亚洲情99在线| xvideos永久免费入口| 亚洲国产精品激情在线观看| 99久久成人国产精品免费| 亚洲伊人久久大香线蕉在观| 国产一级大片免费看| A片在线免费观看| 亚洲综合av一区二区三区不卡| 国产精品亚洲二区在线观看| 四虎免费影院ww4164h| 有码人妻在线免费看片| 亚洲综合一区二区精品久久| 免费又黄又爽的视频| 84pao国产成视频免费播放| 免费人成动漫在线播放r18 | 一级做a爰全过程免费视频| 亚洲午夜无码久久久久小说 | 国产无遮挡吃胸膜奶免费看视频| 国产免费阿v精品视频网址| 亚洲人成无码网站在线观看| 亚洲AV日韩AV永久无码绿巨人 | 免费一级特黄特色大片在线观看| 99爱在线观看免费完整版| 香蕉视频在线观看免费| 亚洲一区二区三区精品视频| 亚洲精品国精品久久99热一| 国产在线观看免费不卡| 97热久久免费频精品99| 免费看黄的成人APP| 免费人成再在线观看网站| 中文文字幕文字幕亚洲色| 无码专区—VA亚洲V天堂| 久久久久亚洲av毛片大| 国产精品免费视频一区| 国产一卡二卡四卡免费| 国产好大好硬好爽免费不卡| 一区二区视频在线免费观看| 亚洲av中文无码字幕色不卡 | 久久久久免费精品国产小说| 亚洲阿v天堂在线2017免费| 国产精品亚洲а∨天堂2021|