事件處理
| 導航 使用者介面 主題: |
Java 平臺事件模型是 Java 平臺上事件驅動程式設計的基礎。
無論你使用什麼 程式語言 或 正規化,你都有可能遇到一個情況,你的程式必須等待外部事件發生。也許你的程式必須等待一些使用者輸入,或者也許它必須等待資料透過網路傳遞。或者可能是其他事情。無論如何,程式必須等待某些超出程式控制範圍的事情發生:程式不能使該事件發生。
在這種情況下,有兩種通用的選擇可以使程式等待外部事件發生。其中第一個稱為輪詢,這意味著你寫一個類似於“當事件未發生時,再次檢查”的小迴圈。輪詢非常容易構建,並且非常直觀。但它也非常浪費:這意味著程式會佔用處理器時間,卻什麼也沒做,只是在等待。對於必須進行大量等待的程式來說,這通常被認為是一個太大的缺點。有很多等待時刻的程式(例如,具有圖形使用者介面的程式,經常不得不長時間等待使用者執行操作),當它們使用另一種機制時,通常會表現得更好:事件驅動程式設計。
在事件驅動程式設計中,必須等待的程式只會進入休眠狀態。它不再佔用處理器時間,甚至可能從記憶體中解除安裝,並且通常使計算機能夠執行有用的操作。但是程式並沒有完全消失;相反,它與計算機或 作業系統 達成了一種協議。一種類似於這樣的協議
好的,作業系統先生,既然我必須等待事件發生,我會暫時離開,讓您繼續做有用的工作。但作為回報,您必須在事件發生時通知我,並讓我回來處理它。
事件驅動程式設計通常對程式的設計有很大的影響。通常,程式需要被分成獨立的部分來進行事件驅動程式設計(一部分用於一般處理,另外一部分或更多部分用於處理發生的事件)。Java 中的事件驅動程式設計比非事件驅動程式設計更復雜,但它可以更有效地利用硬體,有時(比如在開發圖形使用者介面時)將程式碼分成事件驅動的塊實際上非常符合程式的結構。
在本模組中,我們將研究 Java 平臺事件驅動程式設計設施的基礎,並研究該基礎在整個平臺中的一些典型使用示例。
Java 平臺上對事件驅動程式設計的支援最有趣的一點是,實際上沒有這樣的支援。或者,根據您的觀點,平臺中有許多不同的獨立部分提供了他們自己的事件驅動程式設計支援。
Java 平臺沒有提供事件驅動程式設計的通用實現的原因與平臺提供的支援的起源有關。早在 1996 年,Java 程式語言剛剛問世,還在努力立足,並在軟體開發中為自己爭取一席之地。這種早期開發的一部分集中在軟體開發工具上,比如 IDE。當時軟體開發的趨勢之一是面向使用者介面的可重用軟體元件:這些元件將某種有趣、可重用功能封裝到一個單一包中,可以作為一個整體進行處理,而不是作為一組鬆散的獨立類。Sun Microsystems 試圖透過引入他們所謂的 JavaBean 加入元件潮流,這是一個不僅面向 UI 而且可以從 IDE 中輕鬆配置的軟體元件。為了實現這一點,Sun 提出了一個關於 JavaBeans 的大型規範(JavaBeans 規範),主要涉及命名約定(以便從 IDE 中輕鬆處理這些元件)。但 Sun 同時意識到,以 UI 為中心的元件需要支援一種事件驅動的連線方式,將元件中的事件連線到必須由單個開發人員編寫的業務邏輯。因此,JavaBeans 規範還包括一個關於 Java 平臺事件模型的小型規範。
當他們開始著手開發這個事件模型時,Sun 的工程師面臨著一個選擇:試圖提出一個龐大的規範來涵蓋事件模型所有可能的用途,或者只指定一個抽象的、通用的框架,該框架可以在特定情況下進行擴充套件以供個人使用。他們選擇了後者,因此,無論是喜歡還是厭惡,Java 平臺除了這個通用的事件模型框架之外,沒有其他通用的事件驅動程式設計支援。

事件模型框架本身非常簡單,由三個類(一個抽象類)和一個介面組成。最重要的是,它包含程式設計師必須遵守的命名約定。該框架在右側的影像中進行了描述。
從類和介面的角度來看,框架中最重要的部分是 java.util.EventObject 抽象類和 java.util.EventListener 介面。這兩個型別是 Java 平臺事件模型規則和約定的核心,具體包括:
- 當事件發生時需要被通知的類稱為事件監聽器。事件監聽器對它感興趣的每種事件通知型別都有一個不同的方法。
- 事件通知方法宣告被分組到不同的類別中。每個類別都由一個事件監聽器介面表示,該介面必須擴充套件
java.util.EventListener。按照約定,事件監聽器介面命名為<事件類別名稱>Listener。任何將被通知事件的類都必須至少實現一個監聽器介面。 - 與事件發生相關的所有狀態都將捕獲在一個狀態物件中。該物件的類必須是
java.util.EventObject的子類,並且必須至少記錄哪個物件是事件的來源。這樣的類稱為事件類,按照約定命名為<事件類別名稱>Event。 - 通常(但不一定!)一個事件監聽器介面會與一個事件類相關聯。事件監聽器可能具有多個事件通知方法,這些方法接受相同的事件類作為引數。
- 事件通知方法通常(但不一定!)具有傳統的簽名public void <特定事件>(<事件類別名稱>Event evt)。
- 作為事件來源的類必須有一個方法,允許為每種可能的監聽器介面型別註冊監聽器。按照約定,這些方法必須具有簽名public void add<事件類別名稱>Listener(<事件類別名稱>Listener listener)。
- 作為事件來源的類可能有一個方法,允許為每種可能的監聽器介面型別取消註冊監聽器。按照約定,這些方法必須具有簽名public void remove<事件類別名稱>Listener(<事件類別名稱>Listener listener)。

這看起來很多,但是一旦您習慣了就非常簡單。請檢視左側的影像,它包含了一個關於如何使用該框架的通用示例。在這個示例中,我們有一個名為 EventSourceClass 的類,它釋出有趣的事件。遵循事件模型的規則,這些事件由 InterestingEvent 類表示,該類包含指向 EventSourceClass 物件的引用(source,繼承自 java.util.EventObject)。
每當發生有趣事件時,EventSourceClass 必須透過呼叫為此目的存在的通知方法來通知它知道的所有該事件的監聽器。所有通知方法(在本示例中只有一個,interestingEventOccurred)都已按主題分組到一個監聽器介面中:InterestingEventListener,它實現了 java.util.EventListener,並根據事件模型的約定命名。所有事件監聽器類(在本例中只有 InterestingEventListenerImpl)必須實現此介面。由於 EventSourceClass 必須能夠通知所有感興趣的監聽器,因此必須能夠註冊它們。為此,EventSourceClass 有一個 addInterestingEventListener 方法。由於這是必需的,因此還有一個 removeInterestingEventListener 方法。
從示例中可以清楚地看出,使用事件模型主要涉及遵循命名約定。這乍一看可能有點麻煩,但命名約定的目的是允許自動化工具訪問和使用事件模型。事實上,確實存在許多基於這些命名約定的工具、IDE 和框架。
關於事件模型,還有一點需要注意,那就是模型中沒有什麼。事件模型的設計允許實現者在實現選擇方面擁有很大的自由度,這意味著事件模型可以作為各種特定、專用事件處理系統的基礎。
除了命名約定和一些基本類和介面之外,事件模型還指定了以下內容:
- 必須能夠註冊和取消註冊監聽器。
- 事件源必須透過呼叫所有已註冊監聽器上的正確通知方法來發布事件。
- 呼叫事件通知方法是一個正常的、同步的 Java 呼叫,該方法必須由呼叫它的同一個執行緒執行。
但事件模型並沒有指定所有這些如何完成。沒有關於哪些類必須是事件源,以及它們如何跟蹤已註冊的事件監聽器的規則。因此,一個類可能釋出它自己的事件,或者負責釋出與整個物件集合(如整個元件)相關的事件。事件源可能允許在任何時間(即使在處理事件過程中)取消註冊監聽器,或者可能將其限制在某些時間(這與多執行緒有關)。
此外,事件模型沒有指定它如何嵌入到任何程式中。因此,雖然模型指定呼叫事件處理方法是一個同步呼叫,但模型並沒有規定事件處理方法不能將任務委派給另一個執行緒,或者整個事件模型實現必須在應用程式的主執行緒中執行。事實上,Java 平臺的標準使用者介面框架(Swing)包含一個事件處理實現,該實現作為桌面應用程式的完整子系統執行,在它自己的執行緒中執行。
事件通知方法、單播事件處理和事件介面卡
[edit | edit source]在上一節中我們提到,事件通知方法通常接受一個引數。這是首選約定,但規範允許在應用程式確實需要的情況下對此規則進行例外。一個典型的例外情況是,當事件通知必須透過網路以非 Java 方式傳送到遠端系統時,比如 CORBA 標準。在這種情況下,需要多個引數,而事件模型允許這樣做。但是,作為一般規則,通知方法的正確格式是
程式碼段 1.1:簡單通知方法
public void specificEventDescription(Event_type evt)
|
我們之前提到的另一件事是,作為一般規則,事件模型允許許多事件監聽器為同一個事件向同一個事件源註冊。在這種情況下,事件源必須將所有相關事件廣播給所有已註冊的監聽器。但是,事件模型規範再次允許對該規則進行例外。如果從設計角度來看這是必要的,您可能會將事件源限制為註冊單個監聽器;這稱為 *單播事件監聽器註冊*。當使用單播註冊時,如果註冊了太多監聽器,則註冊方法必須宣告為丟擲 java.util.TooManyListenersException 異常
程式碼段 1.2:監聽器註冊
public void add<Event_type>Listener(<Event_type>Listener listener) throws java.util.TooManyListenersException
|

最後,規範允許另一個擴充套件:事件介面卡。事件介面卡是事件監聽器介面的實現,可以插入到事件源和實際的事件監聽器類之間。這是透過使用常規註冊方法將介面卡註冊到事件源物件來完成的。介面卡用於為事件處理機制新增額外的功能,例如事件物件的路由、事件過濾或在實際事件處理類處理之前豐富事件物件。
一個簡單的例子
[edit | edit source]在上一節中,我們探索了 Java 平臺事件模型框架的深度(如果有的話)。如果您像大多數人一樣,您會發現理論文字比模型的實際使用更令人困惑。當然比解釋什麼更令人困惑,實際上是一個非常簡單的框架。
為了澄清一切,讓我們檢查一個基於事件模型框架的簡單示例。假設我們要編寫一個程式,該程式讀取使用者在命令列輸入的數字流並以某種方式處理該流。例如,透過跟蹤數字的執行總和,並在流完全讀取後生成該總和。
當然,我們可以使用 main() 方法中的迴圈非常簡單地實現該程式。但相反,讓我們更具創造性。假設我們想將程式整齊地劃分為類,每個類都有自己的責任(就像我們在適當的面向物件設計中應該做的那樣)。並且讓我們假設我們不僅希望計算所有讀取的數字的總和,而且希望對同一個數字流執行任意數量的計算。實際上,應該能夠相對輕鬆地新增新的計算,而無需影響任何先前存在的程式碼。
如果我們分析這些需求,我們會得出結論,我們在程式中有很多不同的責任
使用事件模型框架允許我們乾淨地分離兩個主要職責,併為我們提供了我們正在尋找的靈活性。如果我們在單個類中實現讀取數字流的邏輯,並將讀取單個數字視為事件,則事件模型允許我們將該事件(以及該數字)廣播到我們喜歡的任意數量的流處理器。讀取數字流的類將充當程式的事件源,每個流處理器將是一個監聽器。由於每個監聽器都是一個獨立的類,並且可以向流讀取器註冊(或不註冊),這意味著我們的模型允許我們擁有多個獨立的流處理,我們可以將其新增到其中,而不會影響讀取流的程式碼或任何預先存在的流處理器。
事件模型表示與事件關聯的任何狀態都應包含在表示事件的類中。這對我們來說很完美;我們可以實現一個簡單的事件類,它將記錄從命令列讀取的數字。然後,每個監聽器都可以根據需要處理此數字。
對於我們有趣的事件集,讓我們保持簡單:讓我們將自己限制為讀取新數字和到達流的末尾。透過這種選擇,我們得到了以下示例應用程式的設計

在接下來的部分中,我們將介紹此示例的實現。
示例基礎知識
[edit | edit source]讓我們從基礎開始。根據事件模型規則,我們必須定義一個事件類來封裝我們有趣的事件。我們應該將這個類命名為 *something-something*Event。讓我們使用 NumberReadEvent,因為這將是我們感興趣的。根據模型規則,該類應該封裝屬於事件發生的任何狀態。在我們的例子中,這就是從流中讀取的數字。我們的事件類必須繼承自 java.util.EventObject。因此,總而言之,以下類就是我們所需要的
程式碼清單 1.1:NumberReadEvent。
package org.wikibooks.en.javaprogramming.example;
import java.util.EventObject;
public class NumberReadEvent extends EventObject {
private double number;
public NumberReadEvent(Object source, Double number) {
super(source);
this.number = number;
}
public double getNumber() {
return number;
}
}
|
接下來,我們必須定義一個監聽器介面。此介面必須定義有趣事件的方法,並且必須擴充套件 java.util.EventListener。我們之前說過我們有趣的事件是“讀取數字”和“到達流的末尾”,所以我們來試試
程式碼清單 1.2:NumberReadListener。
package org.wikibooks.en.javaprogramming.example;
import java.util.EventListener;
public interface NumberReadListener extends EventListener {
public void numberRead(NumberReadEvent numberReadEvent);
public void numberStreamTerminated(NumberReadEvent numberReadEvent);
}
|
實際上,numberStreamTerminated 方法有點奇怪,因為它實際上不是“讀取數字”事件。在實際程式中,您可能希望以不同的方式執行此操作。但讓我們在這個例子中保持簡單。
事件監聽器實現
[edit | edit source]因此,在定義了監聽器介面之後,我們需要一個或多個實現(實際的監聽器類)。至少我們需要一個用來跟蹤讀取的數字的執行總和。當然,我們可以新增任意數量的。但現在讓我們堅持使用一個。顯然,此類必須實現我們的 NumberReadListener 介面。保持執行總和是隨著事件的到來將數字新增到欄位的問題。並且我們希望在到達流的末尾時報告總和;由於我們知道何時發生(即呼叫 numberStreamTerminated 方法),一個簡單的 println 語句就足夠了
程式碼清單 1.3:NumberReadListenerImpl。
package org.wikibooks.en.javaprogramming.example;
public class NumberReadListenerImpl implements NumberReadListener {
double totalSoFar = 0D;
@Override
public void numberRead(NumberReadEvent numberReadEvent) {
totalSoFar += numberReadEvent.getNumber();
}
@Override
public void numberStreamTerminated(NumberReadEvent numberReadEvent) {
System.out.println("Sum of the number stream: " + totalSoFar);
}
}
|
那麼,這段程式碼好嗎?不。它很糟糕,非常糟糕,最重要的是不執行緒安全。但它適合我們的示例。
事件源
[edit | edit source]這裡事情變得有趣:事件源類。這個地方很有趣,因為我們必須在這裡放置程式碼來讀取數字流,程式碼向所有監聽器傳送事件,以及程式碼來 *管理* 監聽器(新增和刪除它們以及跟蹤它們)。
讓我們從考慮跟蹤監聽器開始。通常這很棘手,因為您必須考慮各種多執行緒問題。但在這個例子中我們保持簡單,所以讓我們堅持使用一個簡單的 java.util.Set 的監聽器。我們可以在建構函式中初始化它
程式碼段 1.1:建構函式
private Set<NumberReadListener> listeners;
public NumberReader() {
listeners = new HashSet<NumberReadListener>();
}
|
這種選擇使實現新增和刪除監聽器變得非常容易
程式碼段 1.2:註冊/登出
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
|
在這個示例中,我們實際上不會使用 remove 方法 - 但請記住,模型說它必須存在。
這種簡單選擇的另一個優點是,通知所有監聽器也很容易。我們只需假設所有監聽器都在集中,然後對它們進行迭代即可。由於通知方法是同步的(模型的規則),我們可以直接呼叫它們
程式碼段 1.3:通知器
private void notifyListenersOfEndOfStream() {
for (NumberReadListener numberReadListener : listeners) {
numberReadListener.numberStreamTerminated(new NumberReadEvent(this, 0D));
}
}
private void notifyListeners(Double d) {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(new NumberReadEvent(this, d));
}
}
|
請注意,我們在這裡做了一些假設。首先,我們假設我們將從某個地方獲取 Double 值 d。此外,我們假設沒有監聽器會關心流結束通知中的數字值,並且為此事件傳遞了固定值 0。
最後,我們必須處理讀取數字流。我們將使用 Console 類來完成此操作,並且只需不斷讀取數字,直到沒有更多數字為止
程式碼段 1.4:main 方法
public void start() {
Console console = System.console();
if (console != null) {
Double d = null;
do {
String readLine = console.readLine("Enter a number: ", (Object[])null);
d = getDoubleValue(readLine);
if (d != null) {
notifyListeners(d);
}
} while (d != null);
notifyListenersOfEndOfStream();
}
}
|
請注意,我們如何透過呼叫 notify 方法將數字讀取迴圈掛鉤到事件處理機制?整個類看起來像這樣
程式碼清單 1.4:NumberReader。
package org.wikibooks.en.javaprogramming.example;
import java.io.Console;
import java.util.HashSet;
import java.util.Set;
public class NumberReader {
private Set<NumberReadListener> listeners;
public NumberReader() {
listeners = new HashSet<NumberReadListener>();
}
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
public void start() {
Console console = System.console();
if (console != null) {
Double d = null;
do {
String readLine = console.readLine("Enter a number: ", (Object[])null);
d = getDoubleValue(readLine);
if (d != null) {
notifyListeners(d);
}
} while (d != null);
notifyListenersOfEndOfStream();
}
}
private void notifyListenersOfEndOfStream() {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberStreamTerminated(new NumberReadEvent(this, 0D));
}
}
private void notifyListeners(Double d) {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(new NumberReadEvent(this, d));
}
}
private Double getDoubleValue(String readLine) {
Double result;
try {
result = Double.valueOf(readLine);
} catch (Exception e) {
result = null;
}
return result;
}
}
|
執行示例
[edit | edit source]最後,我們還需要一個類:應用程式的啟動點。此類將包含一個 main() 方法,以及程式碼來建立 NumberReader、一個監聽器以及將兩者組合起來
程式碼清單 1.5:Main。
package org.wikibooks.en.javaprogramming.example;
public class Main {
public static void main(String[] args) {
NumberReader reader = new NumberReader();
NumberReadListener listener = new NumberReadListenerImpl();
reader.addNumberReadListener(listener);
reader.start();
}
}
|
如果您編譯並執行該程式,結果將類似於以下內容
示例執行 >java org.wikibooks.en.javaprogramming.example.Main 輸入一個數字: 0.1 輸入一個數字: 0.2 輸入一個數字: 0.3 輸入一個數字: 0.4 輸入一個數字: |
輸出 數字流的總和: 1.0 |
接下來,讓我們看看如何將介面卡應用於我們的設計。介面卡用於為事件處理過程新增功能,這些功能
- 對於該過程來說是通用的,而不是特定於某個監聽器;或者
- 不應該影響特定監聽器的實現。
根據事件模型規範,介面卡的典型用例是為事件新增路由邏輯。但你也可以新增過濾或日誌記錄。在我們的示例中,讓我們這樣做:將數字作為計算的“證據”記錄到日誌中。
如前所述,介面卡是一個位於事件源和監聽器之間的類。從事件源的角度來看,它偽裝成一個監聽器(因此它必須實現監聽器介面)。從監聽器的角度來看,它假裝是事件源(因此它應該具有新增和刪除方法)。換句話說,要編寫介面卡,你必須從事件源重複一些程式碼(以管理監聽器),並且你必須重新實現事件通知方法以執行一些額外的操作,然後將事件傳遞給實際的監聽器。
在我們的示例中,我們需要一個將數字寫入日誌檔案的介面卡。為了保持簡單,讓我們建立一個介面卡,它
- 使用固定的日誌檔名並用每次程式執行覆蓋該日誌檔案。
- 在建構函式中開啟一個
FileWriter並始終保持開啟狀態。 - 透過將數字寫入
FileWriter來實現numberRead方法。 - 透過關閉
FileWriter來實現numberStreamTerminated方法。
此外,我們可以透過從 NumberReader 類中複製所有我們需要管理監聽器的程式碼來簡化操作。同樣,在實際程式中,你可能想要以不同的方式執行此操作。請注意,每個通知方法的實現也會將事件傳遞給所有真正的監聽器
程式碼清單 1.6:NumberReaderLoggingAdaptor。
package org.wikibooks.en.javaprogramming.example;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class NumberReaderLoggingAdaptor implements NumberReadListener {
private Set<NumberReadListener> listeners;
private BufferedWriter output;
public NumberReaderLoggingAdaptor() {
listeners = new HashSet<NumberReadListener>();
try {
output = new BufferedWriter(new FileWriter("numberLog.log"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
@Override
public void numberRead(NumberReadEvent numberReadEvent) {
try {
output.write(numberReadEvent.getNumber() + "\n");
} catch (Exception e) {
}
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(numberReadEvent);
}
}
@Override
public void numberStreamTerminated(NumberReadEvent numberReadEvent) {
try {
output.flush();
output.close();
} catch (Exception e) {
}
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberStreamTerminated(numberReadEvent);
}
}
}
|
當然,要使介面卡正常工作,我們必須對引導程式碼進行一些更改
程式碼清單 1.7:Main。
package org.wikibooks.en.javaprogramming.example;
public class Main {
public static void main(String[] args) {
NumberReader reader = new NumberReader();
NumberReadListener listener = new NumberReadListenerImpl();
NumberReaderLoggingAdaptor adaptor = new NumberReaderLoggingAdaptor();
adaptor.addNumberReadListener(listener);
reader.addNumberReadListener(adaptor);
reader.start();
}
}
|
但請注意,我們可以在系統中輕鬆重新連結物件。介面卡和監聽器都實現了監聽器介面,並且介面卡和事件源看起來都像事件源,這意味著我們可以將介面卡連線到系統,而無需更改我們之前開發的類中的任何語句。
當然,如果我們執行與上述示例相同的示例,數字現在將記錄在日誌檔案中。
如前所述,事件模型在 Java 平臺中沒有一個單一的、包羅永珍的實現。相反,該模型是幾個不同用途的實現的基礎,這些實現既存在於標準 Java 平臺中,也存在於平臺之外(在框架中)。
在平臺內,主要的實現存在於兩個領域
