CodeGym /課程 /JAVA 25 SELF /建立與處理自訂事件

建立與處理自訂事件

JAVA 25 SELF
等級 50 , 課堂 2
開放

1. 什麼時候需要自訂事件

Java 的標準事件(例如按下按鈕、滑鼠移動或文字變更)只是冰山一角。在實際應用中,會出現大量不在這些範圍內的情境。比方說,程式可能要從網際網路載入資料,需要在載入完成時通知應用程式的其他部分。在遊戲中,玩家可能獲得新的成就,應該同時告知多個元件,例如介面與記錄系統。在企業應用中,訂單狀態的變更應同時觸發對會計、倉庫以及使用者的通知。

在這些情況下,建立自訂的事件與監聽器會很有幫助。它們能描述元件間的獨特互動情境,讓程式碼更具彈性且更有結構。

自訂事件的結構

要在 Java 中建立自己的事件,通常會實作三個部分:

  1. 事件類別 —— 通常繼承自 java.util.EventObject。用來保存事件資訊(來源是誰、與事件相關的資料是什麼)。
  2. 監聽器介面 —— 例如 MyEventListener,它擴充 java.util.EventListener 並定義事件處理方法。
  3. 註冊/移除機制 —— 在事件來源中提供 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();
    }
}

會發生什麼?

  1. DataLoader 載入資料(模擬)。
  2. 載入完成後呼叫 fireDataLoaded,建立事件物件並通知所有監聽器。
  3. 我們的處理器(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:忘記呼叫通知監聽器的方法。 事件被建立了,但原本應該通知監聽器的方法(fireDataLoadedfireCounterChanged)沒有被呼叫。結果是監聽器不會有任何反應。

錯誤 2:監聽器的處理器中丟出例外。 如果某個監聽器拋出例外,其餘監聽器可能收不到通知。較好的做法是用 trycatch 包住對監聽器的呼叫,避免單一「問題」監聽器影響其他人。

錯誤 3:沒有移除不需要的監聽器。 如果監聽器已不再需要但沒有被移除,它仍會持續接收事件且無法自記憶體被釋放。這可能導致記憶體洩漏——特別是當事件來源存活很久、監聽器又很多時。

錯誤 4:在遍歷時修改監聽器清單。 若在事件處理過程中新增或移除監聽器,可能導致 ConcurrentModificationException。較安全的作法是先把清單複製到獨立陣列,再遍歷該副本。

錯誤 5:在處理器中執行耗時操作。 如果處理器進行耗時工作(讀檔、等待網路),介面可能會「卡住」。請把重量級任務移到其他執行緒,或使用非同步機制。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION