1. 什麼時候需要自訂事件
Java 的標準事件(例如按下按鈕、滑鼠移動或文字變更)只是冰山一角。在實際應用中,會出現大量不在這些範圍內的情境。比方說,程式可能要從網際網路載入資料,需要在載入完成時通知應用程式的其他部分。在遊戲中,玩家可能獲得新的成就,應該同時告知多個元件,例如介面與記錄系統。在企業應用中,訂單狀態的變更應同時觸發對會計、倉庫以及使用者的通知。
在這些情況下,建立自訂的事件與監聽器會很有幫助。它們能描述元件間的獨特互動情境,讓程式碼更具彈性且更有結構。
自訂事件的結構
要在 Java 中建立自己的事件,通常會實作三個部分:
- 事件類別 —— 通常繼承自 java.util.EventObject。用來保存事件資訊(來源是誰、與事件相關的資料是什麼)。
- 監聽器介面 —— 例如 MyEventListener,它擴充 java.util.EventListener 並定義事件處理方法。
- 註冊/移除機制 —— 在事件來源中提供 add...Listener/remove...Listener 方法,以及在事件發生時通知監聽器(fire...)。
接下來我們以一個應用程式為例:資料從檔案或網路載入後,需要通知其他人載入已完成。
事件類別
建立一個事件類別,保存載入完成的資訊。
import java.util.EventObject;
// 事件類別:繼承自 EventObject
public class DataLoadedEvent extends EventObject {
private final String data; // 事件的額外資訊
public DataLoadedEvent(Object source, String data) {
super(source); // source — 觸發事件的物件
this.data = data;
}
public String getData() {
return data;
}
}
這裡的 source 是事件來源物件(例如資料載入器),而 data 是包含已載入資料的字串(可以是檔案路徑、JSON、結果等)。
監聽器介面
定義監聽器介面。通常會擴充 EventListener(用於型別標記的介面)。
import java.util.EventListener;
// 我們的事件的監聽器介面
public interface DataLoadedListener extends EventListener {
void dataLoaded(DataLoadedEvent event);
}
dataLoaded 方法會在事件發生時被呼叫。
事件來源
我們需要一個實體,保存監聽器清單,允許註冊/移除它們,並在事件發生時進行通知。
import java.util.ArrayList;
import java.util.List;
public class DataLoader {
private final List<DataLoadedListener> listeners = new ArrayList<>();
// 註冊監聽器
public void addDataLoadedListener(DataLoadedListener listener) {
listeners.add(listener);
}
// 移除監聽器
public void removeDataLoadedListener(DataLoadedListener listener) {
listeners.remove(listener);
}
// 觸發事件的方法(例如資料載入完成後)
private void fireDataLoaded(String data) {
DataLoadedEvent event = new DataLoadedEvent(this, data);
// 通知所有監聽器
for (DataLoadedListener listener : listeners) {
listener.dataLoaded(event);
}
}
// 範例方法,用來「載入」資料
public void loadData() {
// 模擬載入(例如從檔案或網路)
String loadedData = "這是已載入的資料!";
System.out.println("資料已載入:" + loadedData);
// 通知所有監聽器
fireDataLoaded(loadedData);
}
}
在真實情境中,loadData() 可能是非同步、會讀檔或連線到伺服器等,但在此我們僅作簡單模擬。
2. 使用自訂事件
假設我們有個元件,想在資料載入完成時得知這件事。
public class DataLoadedHandler implements DataLoadedListener {
@Override
public void dataLoaded(DataLoadedEvent event) {
System.out.println("處理器收到事件:" + event.getData());
}
}
在應用程式主類別中把一切串起來:
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
DataLoadedHandler handler = new DataLoadedHandler();
// 註冊監聽器
loader.addDataLoadedListener(handler);
// 開始載入資料
loader.loadData();
}
}
會發生什麼?
- DataLoader 載入資料(模擬)。
- 載入完成後呼叫 fireDataLoaded,建立事件物件並通知所有監聽器。
- 我們的處理器(DataLoadedHandler)收到事件並輸出訊息。
輸出範例:
資料已載入:這是已載入的資料!
處理器收到事件:這是已載入的資料!
使用匿名類別與 lambda 運算式
為了避免為每個監聽器都建立獨立類別,常會使用匿名類別或 lambda 運算式(自 Java 8 起):
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
// 透過 lambda 運算式的監聽器
loader.addDataLoadedListener(event ->
System.out.println("Lambda 處理器:" + event.getData())
);
loader.loadData();
}
}
註冊與移除監聽器
監聽器可以新增與移除。這對記憶體管理與避免洩漏很重要(尤其當監聽器是「重量級」物件或已不再需要時)。
DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);
// 後續如果不再需要該處理器:
loader.removeDataLoadedListener(handler);
若未移除監聽器,而事件來源本身存活很久,監聽器會留在清單中且不會被 GC 回收——這可能導致記憶體洩漏。
3. 實作:小型範例 — 點擊計數器
我們做個小範例,適合在教學應用中逐步擴充。假設有一個計數器類別,每次遞增時,會把新值通知給監聽器。
事件類別
import java.util.EventObject;
public class CounterChangedEvent extends EventObject {
private final int newValue;
public CounterChangedEvent(Object source, int newValue) {
super(source);
this.newValue = newValue;
}
public int getNewValue() {
return newValue;
}
}
監聽器介面
import java.util.EventListener;
public interface CounterChangedListener extends EventListener {
void counterChanged(CounterChangedEvent event);
}
計數器類別
import java.util.ArrayList;
import java.util.List;
public class Counter {
private int value = 0;
private final List<CounterChangedListener> listeners = new ArrayList<>();
public void addCounterChangedListener(CounterChangedListener listener) {
listeners.add(listener);
}
public void removeCounterChangedListener(CounterChangedListener listener) {
listeners.remove(listener);
}
public void increment() {
value++;
fireCounterChanged();
}
private void fireCounterChanged() {
CounterChangedEvent event = new CounterChangedEvent(this, value);
for (CounterChangedListener listener : listeners) {
listener.counterChanged(event);
}
}
}
使用方式
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// 透過 lambda 註冊監聽器
counter.addCounterChangedListener(event ->
System.out.println("計數器已變更:" + event.getNewValue())
);
counter.increment(); // 計數器已變更:1
counter.increment(); // 計數器已變更:2
}
}
4. 建立與處理自訂事件時的常見錯誤
錯誤 1:忘記呼叫通知監聽器的方法。 事件被建立了,但原本應該通知監聽器的方法(fireDataLoaded、fireCounterChanged)沒有被呼叫。結果是監聽器不會有任何反應。
錯誤 2:監聽器的處理器中丟出例外。 如果某個監聽器拋出例外,其餘監聽器可能收不到通知。較好的做法是用 try–catch 包住對監聽器的呼叫,避免單一「問題」監聽器影響其他人。
錯誤 3:沒有移除不需要的監聽器。 如果監聽器已不再需要但沒有被移除,它仍會持續接收事件且無法自記憶體被釋放。這可能導致記憶體洩漏——特別是當事件來源存活很久、監聽器又很多時。
錯誤 4:在遍歷時修改監聽器清單。 若在事件處理過程中新增或移除監聽器,可能導致 ConcurrentModificationException。較安全的作法是先把清單複製到獨立陣列,再遍歷該副本。
錯誤 5:在處理器中執行耗時操作。 如果處理器進行耗時工作(讀檔、等待網路),介面可能會「卡住」。請把重量級任務移到其他執行緒,或使用非同步機制。
GO TO FULL VERSION