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 表达式
为了避免为每个监听器都新建单独的类,通常会使用匿名类或(自 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:忘记调用通知监听器的方法。 事件被创建了,但用于通知监听器的方法(fireDataLoaded、fireCounterChanged)没有被调用,导致监听器没有任何反应。
错误 2:监听器处理器中出现异常。 如果某个监听器抛出异常,其他监听器可能收不到通知。推荐做法是将监听器调用包裹在 try–catch 中,使单个“问题”监听器不会影响其他监听器。
错误 3:不移除不再需要的监听器。 如果监听器已不再需要却没有被移除,它会继续接收事件且无法从内存中释放。这可能导致内存泄漏,尤其当事件源存活时间很长而监听器很多时。
错误 4:遍历时修改监听器列表。 如果在事件处理过程中有人添加或移除监听器,可能导致 ConcurrentModificationException。更安全的做法是先将列表复制到单独的数组,然后遍历该副本。
错误 5:在处理器中执行耗时操作。 如果处理器执行耗时任务(加载文件、等待网络等),界面可能会“卡住”。请将重任务放到单独线程中,或使用异步机制。
GO TO FULL VERSION