相信大家都学过Java中的GUI,不知道你们对GUI中的事件机制有没有产生过好奇心,当我们点击按钮时,就可以触发对应的点击事件,这一过程究竟是如何实现的呢?本篇文章我们就来聊一聊Java中的事件监听机制。
在了解事件监听机制之前,我们先来学习一个设计模式——观察者模式,事件监听机制的原理就是它。
场景设置
假设现在有一个需求,你正在运营一个有关天气的接口,要求是可以将天气信息推送出去,前提是接入了该接口的开发者才能收到天气信息,该如何实现呢?
首先我们来创建一个类:
代码语言:javascript复制package com.wwj.spring.guanchazhe;
/**
* 显示天气信息
*/
public class PushWeather {
private int temperature;
private int humidity;
private int airPressure;
public void update(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
show();
}
public void show() {
System.out.print("温度:" temperature "t");
System.out.print("湿度:" humidity "t");
System.out.print("气压:" airPressure "t");
System.out.println();
}
}
该类模拟的是第三方开发者接入我们的数据接口,显示天气信息,其中成员属性分别为温度、湿度和气压,并提供update方法用于更新数据(该方法是由其它类调用的)。
继续创建一个类:
代码语言:javascript复制public class WeatherDataInterface {
private int temperature;
private int humidity;
private int airPressure;
private PushWeather pushWeather;
public WeatherDataInterface(PushWeather pushWeather) {
this.pushWeather = pushWeather;
}
public void update() {
pushWeather.update(temperature, humidity, airPressure);
}
public void updateWeatherData(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
update();
}
}
该类就是天气数据接口类,类中包含了第三方开发者PushWeather,当我们调用updateWeatherData更新接口中的天气信息时,它会同步调用第三方开发者的update方法实现数据同步,下面我们就来试一试:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
PushWeather pushWeather = new PushWeather();
WeatherDataInterface wdi = new WeatherDataInterface(pushWeather);
wdi.updateWeatherData(10, 20, 30);
System.out.println("更新天气数据");
wdi.updateWeatherData(20, 30, 40);
}
}
运行结果:
代码语言:javascript复制温度:10 湿度:20 气压:30
更新天气数据
温度:20 湿度:30 气压:40
这种实现方式是有很大弊端的,因为如果又有一个第三方开发者要接入你的接口,那么修改的代码将会非常多,不信来看看,首先创建第三方开发者:
代码语言:javascript复制public class Baidu {
private int temperature;
private int humidity;
private int airPressure;
public void update(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
show();
}
public void show() {
System.out.print("百度接入————温度:" temperature "t");
System.out.print("百度接入————湿度:" humidity "t");
System.out.print("百度接入————气压:" airPressure "t");
System.out.println();
}
}
然后需要修改的是我们的天气数据接口:
代码语言:javascript复制public class WeatherDataInterface {
private int temperature;
private int humidity;
private int airPressure;
private PushWeather pushWeather;
private Baidu baidu;
public WeatherDataInterface(PushWeather pushWeather,Baidu baidu) {
this.pushWeather = pushWeather;
this.baidu = baidu;
}
public void update() {
pushWeather.update(temperature, humidity, airPressure);
baidu.update(temperature,humidity,airPressure);
}
public void updateWeatherData(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
update();
}
}
首先需要添加百度到成员变量,然后修改构造方法, 还需要修改update方法,让其也能更新百度的数据,测试代码:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
PushWeather pushWeather = new PushWeather();
Baidu baidu = new Baidu();
WeatherDataInterface wdi = new WeatherDataInterface(pushWeather,baidu);
wdi.updateWeatherData(10, 20, 30);
System.out.println("更新天气数据");
wdi.updateWeatherData(20, 30, 40);
}
}
运行结果:
代码语言:javascript复制温度:10 湿度:20 气压:30
百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30
更新天气数据
温度:20 湿度:30 气压:40
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40
观察者模式
观察者模式,又被称为发布——订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当该主题对象发生数据变化时,会通知所有的观察者对象更新数据。
很显然,在刚才的案例中,第三方开发者就是观察者模式中的观察者,而天气数据接口就是主题对象,当天气数据接口发生变化时,就会通知那些依赖于天气接口的观察者去更新自己的数据,所以刚才的案例是非常适合使用观察者模式来进行改造的,那怎么实现呢?
观察者模式中有几个非常重要的概念:
- Subject:抽象主题,它是用于抽象观察者的,因为主题对象需要管理所有依赖于它的观察者,所以必须对观察者抽象,才能实现统一的管理,提供接口注册和注销观察者
- ConcreteSubject:具体主题,它用于具体实现主题对象,它会将有关状态存入具体的观察者对象,在具体主题数据发生变化时,会给所有已经注册的观察者发送通知
- Observer:抽象观察者,它定义了一个接口,用于对观察者进行抽象
- ConcreteObserver:具体观察者,实现抽象观察者接口,以便在得到主题对象的通知时更新自身数据
现在我们就来改造刚才的案例,首先创建抽象主题:
代码语言:javascript复制public interface Subject {
// 注册观察者对象
void register(Observer observer);
// 移除观察者对象
void remove(Observer observer);
// 通知所有观察者更新数据
void notify(int temperature, int humidity, int airPressure);
}
然后创建抽象观察者:
代码语言:javascript复制public interface Observer {
// 更新天气数据
void update(int temperature, int humidity, int airPressure);
}
接着具体实现主题:
代码语言:javascript复制public class WeatherDataSubject implements Subject {
// 管理所有观察者
private Vector<Observer> vector;
public WeatherDataSubject() {
vector = new Vector<>();
}
@Override
public void register(Observer observer) {
vector.add(observer);
}
@Override
public void remove(Observer observer) {
vector.remove(observer);
}
@Override
public void notify(int temperature, int humidity, int airPressure) {
for (Observer observer : vector) {
observer.update(temperature, humidity, airPressure);
}
}
}
最后就是创建具体的观察者,也就是第三方开发者:
代码语言:javascript复制public class Baidu implements Observer {
private int temperature;
private int humidity;
private int airPressure;
public void show() {
System.out.print("百度接入————温度:" temperature "t");
System.out.print("百度接入————湿度:" humidity "t");
System.out.print("百度接入————气压:" airPressure "t");
System.out.println();
}
@Override
public void update(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
show();
}
}
编写测试代码:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
Baidu baidu = new Baidu();
WeatherDataSubject subject = new WeatherDataSubject();
subject.register(baidu);
subject.notify(10, 20, 30);
System.out.println("更新天气数据");
subject.notify(20, 30, 40);
}
}
运行结果:
代码语言:javascript复制百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30
更新天气数据
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40
现在若是想接入新的第三方开发者,那就变得非常简单了,首先创建新的开发者:
代码语言:javascript复制public class Alibaba implements Observer {
private int temperature;
private int humidity;
private int airPressure;
public void show() {
System.out.print("阿里巴巴接入————温度:" temperature "t");
System.out.print("阿里巴巴接入————湿度:" humidity "t");
System.out.print("阿里巴巴接入————气压:" airPressure "t");
System.out.println();
}
@Override
public void update(int temperature, int humidity, int airPressure) {
this.temperature = temperature;
this.humidity = humidity;
this.airPressure = airPressure;
show();
}
}
然后修改测试代码即可:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
Baidu baidu = new Baidu();
Alibaba alibaba = new Alibaba();
WeatherDataSubject subject = new WeatherDataSubject();
subject.register(baidu);
subject.register(alibaba);
subject.notify(10, 20, 30);
System.out.println("更新天气数据");
subject.notify(20, 30, 40);
}
}
运行结果:
代码语言:javascript复制百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30
阿里巴巴接入————温度:10 阿里巴巴接入————湿度:20 阿里巴巴接入————气压:30
更新天气数据
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40
阿里巴巴接入————温度:20 阿里巴巴接入————湿度:30 阿里巴巴接入————气压:40
通过观察者模式极大地解除了程序间的耦合,虽然主题对象中仍然依赖了一个集合类型,但它已经被抽象化了,所以耦合度其实并不算很高,通过这种方式,我们在接入新的开发者时,只需向主题对象注册即可,若是不想接入了,也可以注销该开发者。
事件监听机制
了解观察者模式之后,我们进入本篇文章的重心,事件监听机制。
在该模型中,有三个非常重要的概念:
- 事件
- 事件源
- 事件监听器
其具体流程是:用户操作(比如点击)导致事件触发,前提是事件监听器已经被注册好了,事件触发后会生成事件对象,此时事件对象会作为参数传递给事件监听器,监听器调用对应的方法进行处理。
在这里事件源就是主题对象,而事件监听器就是观察者,当事件源发生变化时,主题对象就会通知所有的观察者处理数据,那么接下来我们就来实现一下。
首先创建事件接口:
代码语言:javascript复制public interface Event {
// 事件回调
void callback();
}
然后创建具体实现:
代码语言:javascript复制public class ValueEvent implements Event {
// 事件三要素:事件源、事件发生事件、事件消息
private Object source;
private LocalDateTime when;
private String msg;
public void setSource(Object source) {
this.source = source;
}
public void setWhen(LocalDateTime when) {
this.when = when;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getSource() {
return source;
}
public LocalDateTime getWhen() {
return when;
}
public String getMsg() {
return msg;
}
@Override
public String toString() {
return "ValueEvent{"
"source=" source
", when=" when
", msg='" msg '''
'}';
}
@Override
public void callback() {
System.out.println(this);
}
}
创建监听器接口:
代码语言:javascript复制public interface EventListener {
// 触发事件
void triggerEvent(Event event);
}
实现监听器:
代码语言:javascript复制public class ValueChangeListener implements EventListener {
@Override
public void triggerEvent(Event event) {
// 调用事件回调方法
event.callback();
}
}
最后编写事件源接口:
代码语言:javascript复制public interface EventSource {
// 注册监听器
void addListener(EventListener listener);
// 通知所有监听器
void notifyListener();
}
实现事件源接口:
代码语言:javascript复制public class ValueSource implements EventSource {
// 管理所有监听器
private Vector<EventListener> listeners;
private String msg;
public ValueSource() {
listeners = new Vector<>();
}
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
}
@Override
public void notifyListener() {
for (EventListener listener : listeners) {
ValueEvent event = new ValueEvent();
event.setSource(this);
event.setWhen(LocalDateTime.now());
event.setMsg("更新数据:" msg);
}
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
notifyListener();
}
}
编写测试代码:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
ValueSource source = new ValueSource();
source.addListener(new ValueChangeListener());
source.setMsg("50");
}
}
运行结果:
代码语言:javascript复制ValueEvent{source=com.wwj.spring.guanchazhe.click.ValueSource@1d81eb93, when=2021-05-22T13:19:26.806, msg='更新数据:50'}
我们来仔细分析一下这个过程,首先我们创建了一个事件源:
代码语言:javascript复制ValueSource source = new ValueSource();
它相当于观察者模式中的主题对象,也就是被观察者,当被观察者数据发生变化时,通知所有监听器进行处理,所以我们为其注册了一个监听器:
代码语言:javascript复制source.addListener(new ValueChangeListener());
此时我们修改事件源的数据:
代码语言:javascript复制source.setMsg("50");
就会执行setMsg方法:
代码语言:javascript复制public void setMsg(String msg) {
this.msg = msg;
notifyListener();
}
该方法又调用了notifyListener方法,通知所有监听器处理:
代码语言:javascript复制@Override
public void notifyListener() {
for (EventListener listener : listeners) {
ValueEvent event = new ValueEvent();
event.setSource(this);
event.setWhen(LocalDateTime.now());
event.setMsg("更新数据:" msg);
listener.triggerEvent(event);
}
}
在该方法中,首先需要创建事件,并设置事件源,也就是当前对象,设置事件发生时间和消息,最后调用监听器的事件处理方法:
代码语言:javascript复制@Override
public void triggerEvent(Event event) {
// 调用事件回调方法
event.callback();
}
该方法又调用了事件的回调方法:
代码语言:javascript复制@Override
public void callback() {
System.out.println(this);
}
事件回调方法就输出了当前对象,以上就是整个事件监听机制的流程。
总结
最后,我们通过这张图,再总结一下事件监听的整个流程:
- 首先创建事件源,并为其注册事件
- 当调用setMsg方法修改事件源中的数据时,会调用notifyListener方法通知所有监听器
- 在notifyListener方法中会遍历所有的监听器,创建事件对象,并作为参数传入监听器的事件处理方法(triggerEvent)
- 监听器的triggerEvent方法会调用事件的回调方法(callback)
- 回调方法用于编写具体的处理逻辑,比如输出内容给用户反馈