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 表达式

为了避免为每个监听器都新建单独的类,通常会使用匿名类或(自 Java 8 起的)lambda 表达式:

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);

如果不移除监听器,而事件源本身生命周期很长,监听器会一直留在列表中,无法被垃圾回收,这可能导致内存泄漏。

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:在处理器中执行耗时操作。 如果处理器执行耗时任务(加载文件、等待网络等),界面可能会“卡住”。请将重任务放到单独线程中,或使用异步机制。

1
任务
JAVA 25 SELF, 第 50 级, 课程 2
已锁定
魔法脉冲通信 ✨
魔法脉冲通信 ✨
1
任务
JAVA 25 SELF, 第 50 级, 课程 2
已锁定
魔法脉冲接收器 🔮
魔法脉冲接收器 🔮
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION