官术网_书友最值得收藏!

2.3 Java中的事件編程

上面觀察者模式風格的代碼是用Java語言編寫的,本節將以它為起點探討用Java語言進行事件編程的各種可能性,比較它們的優劣,并以此為背景揭示Java 8引入Lambda表達式和方法引用的意義。

2.3.1 通用的事件發布者和收聽者

2.2節的Java代碼有一個問題,就是事件發布者只能觸發一種事件。在實際編程中,發布者往往需要區分多種不同意義的事件,例如鼠標的單擊、移動、懸停。為此可以重構EventPublisher類,在添加、刪除和調用事件收聽者的方法中區分不同的類型。

2.3.2 通用事件收聽者的問題

利用上面的通用EventPublisher類,可以給代碼添加任意的事件,每個事件傳遞給收聽者的信息,也可以利用EventInfo的put()方法自定義。然而在實際圖形用戶界面開發中使用它們,還會遇到一個問題。在向事件發布者添加收聽者時,需要一個特定的實現IEventListener接口的類。最簡潔的方式是就地創建一個匿名類,就像上面的代碼那樣。但在實際開發中,經常將構建用戶界面的代碼和收聽者的代碼分置于不同的類,這樣就必須在調用用戶界面對象的addEventListener()方法時傳入收聽者的實例。除了讓對象的職責更加清晰,將兩者分開還有一個可能的理由,就是有時用戶界面上會有多個作用相同或類似的控件,例如分頁列表上下方相同的翻頁按鈕和列表中每一行都有的功能鏈接,這些控件的事件處理程序是通用的,應該共用一個收聽者,而不是為每個控件創建一個一模一樣的(在第3章中我們還會分析用戶界面及其事件收聽者代碼分開的方式和是否必要)。當我們將事件處理程序放在一個單獨的類中時就會發現,雖然一個事件發布者可以發布多個事件,一個收聽者卻只能處理一種事件。這就意味著為用戶界面類的每一種事件都必須創建一個收聽者類,而這顯然是極為麻煩的。這個問題自然的解決思路就是讓事件收聽者和發布者一樣,能夠處理多種事件。這正是AWT和Swing圖形用戶界面類庫采用的方案。

2.3.3 Swing用戶界面里的事件編程

下面介紹Oracle網站上Java教程里“How to Write a Mouse Listener”一文里對編寫鼠標事件響應程序的范例(http://docs.oracle.com/javase/tutorial/uiswing/events/mouselistener.html)。

程序給一個擴展自JLabel的BlankArea類和一個JPanel控件添加了同一個事件收聽者,該收聽者會處理鼠標進入、退出、鼠標鍵按下、釋放和單擊五種事件。用戶界面、事件處理程序和入口方法都混雜在一起,為了更清晰地展示事件發布者和收聽者的關系,可以調整一下代碼的結構。

2.3.4 專用事件收聽者的問題

不妨把MouseListener這樣的專門針對某種事件的收聽者稱為專用的事件收聽者,以和之前通用的事件收聽者對比。在2.3.3節的代碼中,一個和用戶界面分開的收聽者Controller可以處理用戶界面上多個控件的多種事件,通用事件收聽者的問題似乎完滿解決了。然而如果觀察一下作為事件發布者的控件的內部代碼,就會發現與之前的通用EventPublisher類比較,與事件有關的代碼復雜了很多。

無須注意各個方法的細節,我們能看出的規律是:為了一組鼠標事件單擊、按鍵、釋放、進入和離開,創建了一個MouseEvent類,用于封裝這些事件共同的信息;創建了一個MouseListener接口,為每一種事件指定了一個處理方法,這些事件的任何收聽者都要實現這個接口;在Component類中為這一組事件編寫了addMouseListener、removeMouseListener和processMouseEvent方法,前兩者分別為控件添加和移除這些鼠標事件的收聽者,任何一個用戶界面事件發生時,processEvent方法先根據事件的類型調用相應的處理函數,如果屬于這里討論的鼠標事件,就調用processMouseEvent方法,它再次根據事件的類型調用收聽者中對應的方法。

以上是事件發布者為了一組事件做的所有準備工作。控件要發布的事件很多,例如MouseMotionEvent、MouseWheelEvent、KeyEvent,除了事件信息類有可能共用,對每一組事件都要重復類似的套路。一組事件有可能有很多個,也可能只有一個,分組的依據僅僅是它們在性質上可視為同屬一個類別以及事件信息可以共用一個類。上述事件在Java最初的AWT圖形用戶界面框架中就被支持,到了后來的Swing會不會簡單一些呢?下面介紹JMenu類發布的菜單事件。

可以看出作為事件發布者的代碼,仍然既不簡便也沒有重用,為了菜單的selected、deselected和canceled三個事件每個都編寫一個內容基本重復的方法。實際上,剛剛所說的套路是Java 8發布之前Java世界里事件編程的標準寫法,不僅是來自圖形用戶界面的事件,程序員為對象添加自定義事件也遵循這樣的模式。為了一個事件這樣大費周章,原因不止一個。了解這些原因可以更好地認識事件和面向對象編程都有好處。

2.3.5 徹底地面向對象

很多人在開始接觸Java時,對Hello World在Java里的寫法不習慣:

奇怪為什么要這樣麻煩,在main方法里創建一個對象,再調用它的方法。而不是像在C等語言里那樣簡單:

后來漸漸明白了,作為徹底實踐面向對象設計的編程語言,Java的“邏輯單元”是對象。這句斷言有兩層含義。第一層含義是Java代碼的組織單元是類。所謂組織單元,就是指能夠獨立存在和運行的代碼的最小單位。C這樣的過程式語言的組織單元是過程(函數)。C++雖然引入了面向對象的設計,仍然允許以過程的方式組織代碼。換句話說,C語言里一般的語句(除了聲明變量和初始化等)都要寫在某個函數里;而在Java中,一般的語句不僅要寫在某個函數里,而且每個函數都要位于某個類中(即作為類的方法)。所以在Hello World程序中,Java要把“System.out.println("Hello World!")”;這條簡單的語句置于一個類中,還要以一個對象的方法的形式來運行它。第二層含義是在Java中一切都是對象(此處讓我們把int這樣的原始類型也當成特殊的對象),變量指向的、方法的參數傳遞的都是對象。這一點上C與Java最大的差異是存在函數指針,也就是說,函數可以和其他數據一樣作為參數傳遞。正是Java的這個特點使得在其中的事件編程呈現出上一節的樣貌。

在上一節的討論中已經看到,事件發布者會發布多種事件,收聽者會包含感興趣的多個事件的處理程序。問題是怎樣將某個事件映射到收聽者內對應的處理函數。因為函數在Java中無法獨立存在,既不能從收聽者直接傳遞給發布者,也不能被發布者保留,所以只好將它們的容器——收聽者傳遞給發布者,發布者內保持收聽者的列表。那么,當某個事件發生時,發布者如何知道應該調用收聽者的哪個函數呢?沒有其他辦法,只能約定函數的名稱。所以在上一節里,EventPublisher類每當事件發生時都調用收聽者的handleEvent方法,AWT和Swing中控件每當鼠標事件發生時就分別調用收聽者的mousePressed、mouseClicked等方法,當菜單事件發生時就分別調用menuSelected、menuCanceled等方法。Java又是靜態強類型的語言,調用一個對象的方法在編譯時要進行類型檢查。為了確保事件收聽者擁有那些約定的方法,必須創建一個接口(如MouseListener)來包含這些方法,然后收聽者實現此接口,發布者在添加、刪除收聽者和調用其方法時也只使用該接口類型。類似地,為了對發布者傳遞給收聽者的事件信息對象的屬性進行編譯時檢查,需要給該信息對象創建一個特定于事件的類型(如MouseEvent)。事件收聽者接口和信息對象相互匹配,通常為了一組相近的事件創建兩者。再來看事件發布者,因為它在添加、刪除收聽者時使用的是特定于事件的接口,所以不能有EventPublisher中那樣的通用方法addEventListener和removeEventListener,而只能為每一組事件都編寫一對類似于addMouseListener和removeMouseListener的特定方法。發布者觸發事件時,理論上可以在一個方法中完成,但為了代碼清晰,通常會為每一組事件都編寫一個方法(如processMouseEvent),有時更因為容納收聽者的容器的復雜性,為一組事件里的每一種都編寫一個單獨的方法(參看上一節的fireMenuSelected、fireMenuDeselected和fireMenuCanceled)。至此,在Java中進行事件編程的拼圖就完整了,對一組具體的事件,總計需要一個事件信息類型、一個收聽者接口、一個對該接口的實現、一個包含若干特定方法的發布者。

盡管工作量不小,這個方案仍然不能應對實際開發中稍微復雜一點的場合。用戶在視窗的控件上做的動作觸發它們的各種事件,選擇恰當的事件編寫處理程序是圖形界面程序和用戶交互的途徑。從前面/上文可以看到,Java中的收聽者能夠處理控件發布的多種事件,對處理邏輯相同的事件,還能一對多地服務多個控件,可是對多個控件的處理邏輯不同的同一種事件卻無能為力。最簡單和常見的情形就是視窗上有多個按鈕,每個的功能都不同,收聽者照例要實現MouseListener接口的mouseClicked方法,但一個收聽者的mouseClicked方法只能包含一個按鈕的處理邏輯(將所有按鈕的處理邏輯混合在一起或者再分配到子函數,雖然理論上可行,代碼的結構卻會變得不自然而難以理解),結果就是為每個按鈕創建一個收聽者,程序變得十分繁冗。究其原因,還是在Java中函數不能作為參數傳遞,不能保存在變量中。

2.3.6 Java 8帶來的福音

前面分析的Java事件編程的局限和不便終于在Java 8發布之后見到了曙光。隨著近年來函數式編程的流行,許多語言都引入了Lambda表達式的功能。千呼萬喚之后,Java中的Lambda表達式也姍姍而來。Lambda表達式是函數式編程的基石,與命令式編程中的函數相比,其特點是與普通數據類型的值一樣,能夠被賦予變量,作為參數傳給其他函數,作函數的返回值,也就是所謂的一級(first-class)函數。簡言之,Lambda表達式是可以運行的數據。在Java中,Lambda表達式是以特殊語法的匿名函數的形式定義的。因而在給事件發布者添加就地定義的收聽者時,比原來的匿名類更加簡潔。

如果想把事件處理程序放在和用戶界面分開的類中,上面的Lambda表達式可以簡單引用該類的方法。為了類似這樣的場合,Java引入了方法引用(Method reference),于是發布者在添加事件處理程序時可以直接引用另一個收聽者對象的方法。

利用這些Java 8帶來的新功能,事件編程現在能夠以一種優雅的方式進行:收聽者接口是唯一的、通用的;發布者內添加、刪除和調用收聽者的方法也是通用的;發布者和收聽者可以分開定義,并且一個收聽者可以包含任意多個發布者的任意多種事件的處理方法。Java新的圖形用戶界面框架JavaFX正是這樣:EventHandler <T extends Event>是通用的收聽者接口,Event和EventType <T extends Event>類層次分別代表事件信息和類型,圖形界面控件的基類Node有一組addEventHandler(EventType <T> eventType,EventHandler <? super T> eventHandler)、removeEventHandler(EventType <T> eventType,EventHandler <? super T> eventHandler)這樣的接收通用收聽者接口的方法,和一批為了方便使用特定事件收聽者接口的方法,setOnMouseClicked(EventHandler <? super MouseEvent> value)、EventHandler <? super MouseEvent> getOnMouseClicked()……

下面的代碼片段演示的就是在與用戶界面分離的收聽者類內為一個按鈕添加事件處理方法。

     myButton.setOnAction(this::handleButtonAction);
     private void handleButtonAction(ActionEvent event){
     //...
     }

Oracle公司的JavaFX只面向桌面環境,移動環境如Android下的Java開發,雖然也有非官方組織做的移植,但普遍還是使用Android的原生GUI框架。不過該框架中事件編程的View.OnClickListener、View.OnLongClickListener等收聽者接口與JavaFX的EventHandler <T extends Event>一樣,也是函數式接口,只要啟用名為Jack的新編譯器和相應的開發工具,Android下的圖形用戶界面程序事件編程也能使用上述Java 8的新功能。

2.3.7 這一切背后仍然是對象

Lambda表達式和方法引用似乎表明在Java中方法可以像對象一樣傳遞了,然而實際上Java仍然固執地堅持著包含方法的對象才能作為數據使用的原則。Lambda表達式和方法引用背后不是一般函數式編程語言中的函數,而是某個函數式接口(Functional interface)的對象。

在Java中,一個接口如果只定義了一個抽象方法,就稱為函數式接口。例如上文中的通用事件收聽者接口、用于比較的Comparator <T>接口等。因為只包含一個方法,這種接口的實現類往往實質上就是充當該方法的包裝。將一個函數式接口的實例賦予某個變量,作為參數傳遞給某個方法,作為某個方法的返回值,就以對象的形式實現了前文所說的一級函數的特點。函數式接口中的方法定義則保證了靜態強類型語言對此一級函數簽名的編譯時類型檢查。Java中Lambda表達式和方法引用的背后都是函數式接口的對象,這在下面的代碼里體現得很清楚。

所以在上一節的樣例代碼中,添加Lambda表達式和方法引用形式的事件收聽者,本質和傳統的添加接口形式的收聽者是一樣的,只是創建同一類型的接口實例的語法上更便捷的方式。在IDE中將鼠標指針懸浮于該Lambda表達式和方法引用上方,也能看到它們的類型是IEventListener。所不同的是,對于普通接口,一個對象只能從整體上實現一次。也就是我們在2.3.5節中所說的,一個實現了MouseListener接口的收聽者只有一個mouseClicked方法,所以只能處理一個控件的單擊事件。用方法引用形式創建的函數式接口實例則不然,只要簽名符合要求,一個對象中的每個方法都能創建一個包裝它的接口實例。正是這種能力,給事件編程帶來了上節所述的變化。

另外值得指出的是,函數式接口雖然是方法引用所依托的類型,但它本身由來已久,Runnable、Comparator這些接口在Java引入@FunctionalInterface標記以前就符合函數式接口的定義,在Java 8新增Lambda表達式和方法引用功能之前,函數式接口和普通接口的用法毫無二致。比如Java的另一圖形用戶界面框架SWT,供外界使用的事件收聽者接口org.eclipse.swt.events.KeyListener與Swing類似都是專用的包含多個方法的,SWT內部使用的則是通用的只包含一個方法的org.eclipse.swt.widgets.Listener接口,然而這個函數式接口在Java 8之前仍然面臨前面分析的通用事件收聽者的問題。

主站蜘蛛池模板: 应城市| 精河县| 建瓯市| 泰安市| 武功县| 巍山| 军事| 沽源县| 沙河市| 田东县| 法库县| 苍山县| 罗江县| 闻喜县| 吴旗县| 泰兴市| 大竹县| 遂平县| 措美县| 梅州市| 自贡市| 永胜县| 镇巴县| 江陵县| 长泰县| 西乌| 河北区| 宜州市| 修文县| 林芝县| 永春县| 淮北市| 饶河县| 龙游县| 衡水市| 永济市| 自贡市| 泰州市| 锦屏县| 潍坊市| 沙洋县|