聊一聊Java中的事件監(jiān)聽(tīng)機(jī)制
汪偉俊 | 作者
Java技術(shù)迷 | 出品
相信大家都學(xué)過(guò)Java中的GUI,不知道你們對(duì)GUI中的事件機(jī)制有沒(méi)有產(chǎn)生過(guò)好奇心,當(dāng)我們點(diǎn)擊按鈕時(shí),就可以觸發(fā)對(duì)應(yīng)的點(diǎn)擊事件,這一過(guò)程究竟是如何實(shí)現(xiàn)的呢?本篇文章我們就來(lái)聊一聊Java中的事件監(jiān)聽(tīng)機(jī)制。
在了解事件監(jiān)聽(tīng)機(jī)制之前,我們先來(lái)學(xué)習(xí)一個(gè)設(shè)計(jì)模式——觀察者模式,事件監(jiān)聽(tīng)機(jī)制的原理就是它。
場(chǎng)景設(shè)置
假設(shè)現(xiàn)在有一個(gè)需求,你正在運(yùn)營(yíng)一個(gè)有關(guān)天氣的接口,要求是可以將天氣信息推送出去,前提是接入了該接口的開(kāi)發(fā)者才能收到天氣信息,該如何實(shí)現(xiàn)呢?
首先我們來(lái)創(chuàng)建一個(gè)類:
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();
}
}
該類模擬的是第三方開(kāi)發(fā)者接入我們的數(shù)據(jù)接口,顯示天氣信息,其中成員屬性分別為溫度、濕度和氣壓,并提供update方法用于更新數(shù)據(jù)(該方法是由其它類調(diào)用的)。
繼續(xù)創(chuàng)建一個(gè)類:
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();
}
}
該類就是天氣數(shù)據(jù)接口類,類中包含了第三方開(kāi)發(fā)者PushWeather,當(dāng)我們調(diào)用updateWeatherData更新接口中的天氣信息時(shí),它會(huì)同步調(diào)用第三方開(kāi)發(fā)者的update方法實(shí)現(xiàn)數(shù)據(jù)同步,下面我們就來(lái)試一試:
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("更新天氣數(shù)據(jù)");
wdi.updateWeatherData(20, 30, 40);
}
}
運(yùn)行結(jié)果:
溫度:10 濕度:20 氣壓:30
更新天氣數(shù)據(jù)
溫度:20 濕度:30 氣壓:40
這種實(shí)現(xiàn)方式是有很大弊端的,因?yàn)槿绻钟幸粋€(gè)第三方開(kāi)發(fā)者要接入你的接口,那么修改的代碼將會(huì)非常多,不信來(lái)看看,首先創(chuàng)建第三方開(kāi)發(fā)者:
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("百度接入————?dú)鈮?" + airPressure + "\t");
System.out.println();
}
}
然后需要修改的是我們的天氣數(shù)據(jù)接口:
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();
}
}
首先需要添加百度到成員變量,然后修改構(gòu)造方法, 還需要修改update方法,讓其也能更新百度的數(shù)據(jù),測(cè)試代碼:
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("更新天氣數(shù)據(jù)");
wdi.updateWeatherData(20, 30, 40);
}
}
運(yùn)行結(jié)果:
溫度:10 濕度:20 氣壓:30
百度接入————溫度:10 百度接入————濕度:20 百度接入————?dú)鈮?30
更新天氣數(shù)據(jù)
溫度:20 濕度:30 氣壓:40
百度接入————溫度:20 百度接入————濕度:30 百度接入————?dú)鈮?40
觀察者模式
觀察者模式,又被稱為發(fā)布——訂閱模式,它定義了一種一對(duì)多的依賴關(guān)系,讓多個(gè)觀察者對(duì)象同時(shí)監(jiān)聽(tīng)某一個(gè)主題對(duì)象,當(dāng)該主題對(duì)象發(fā)生數(shù)據(jù)變化時(shí),會(huì)通知所有的觀察者對(duì)象更新數(shù)據(jù)。很顯然,在剛才的案例中,第三方開(kāi)發(fā)者就是觀察者模式中的觀察者,而天氣數(shù)據(jù)接口就是主題對(duì)象,當(dāng)天氣數(shù)據(jù)接口發(fā)生變化時(shí),就會(huì)通知那些依賴于天氣接口的觀察者去更新自己的數(shù)據(jù),所以剛才的案例是非常適合使用觀察者模式來(lái)進(jìn)行改造的,那怎么實(shí)現(xiàn)呢?
觀察者模式中有幾個(gè)非常重要的概念:
Subject:抽象主題,它是用于抽象觀察者的,因?yàn)橹黝}對(duì)象需要管理所有依賴于它的觀察者,所以必須對(duì)觀察者抽象,才能實(shí)現(xiàn)統(tǒng)一的管理,提供接口注冊(cè)和注銷觀察者 ConcreteSubject:具體主題,它用于具體實(shí)現(xiàn)主題對(duì)象,它會(huì)將有關(guān)狀態(tài)存入具體的觀察者對(duì)象,在具體主題數(shù)據(jù)發(fā)生變化時(shí),會(huì)給所有已經(jīng)注冊(cè)的觀察者發(fā)送通知 Observer:抽象觀察者,它定義了一個(gè)接口,用于對(duì)觀察者進(jìn)行抽象 ConcreteObserver:具體觀察者,實(shí)現(xiàn)抽象觀察者接口,以便在得到主題對(duì)象的通知時(shí)更新自身數(shù)據(jù)
現(xiàn)在我們就來(lái)改造剛才的案例,首先創(chuàng)建抽象主題:
public interface Subject {
// 注冊(cè)觀察者對(duì)象
void register(Observer observer);
// 移除觀察者對(duì)象
void remove(Observer observer);
// 通知所有觀察者更新數(shù)據(jù)
void notify(int temperature, int humidity, int airPressure);
}
然后創(chuàng)建抽象觀察者:
public interface Observer {
// 更新天氣數(shù)據(jù)
void update(int temperature, int humidity, int airPressure);
}
接著具體實(shí)現(xiàn)主題:
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);
}
}
}
最后就是創(chuàng)建具體的觀察者,也就是第三方開(kāi)發(fā)者:
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("百度接入————?dú)鈮?" + 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();
}
}
編寫(xiě)測(cè)試代碼:
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("更新天氣數(shù)據(jù)");
subject.notify(20, 30, 40);
}
}
運(yùn)行結(jié)果:
百度接入————溫度:10 百度接入————濕度:20 百度接入————?dú)鈮?30
更新天氣數(shù)據(jù)
百度接入————溫度:20 百度接入————濕度:30 百度接入————?dú)鈮?40
現(xiàn)在若是想接入新的第三方開(kāi)發(fā)者,那就變得非常簡(jiǎn)單了,首先創(chuàng)建新的開(kāi)發(fā)者:
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("阿里巴巴接入————?dú)鈮?" + 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();
}
}
然后修改測(cè)試代碼即可:
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("更新天氣數(shù)據(jù)");
subject.notify(20, 30, 40);
}
}
運(yùn)行結(jié)果:
百度接入————溫度:10 百度接入————濕度:20 百度接入————?dú)鈮?30
阿里巴巴接入————溫度:10 阿里巴巴接入————濕度:20 阿里巴巴接入————?dú)鈮?30
更新天氣數(shù)據(jù)
百度接入————溫度:20 百度接入————濕度:30 百度接入————?dú)鈮?40
阿里巴巴接入————溫度:20 阿里巴巴接入————濕度:30 阿里巴巴接入————?dú)鈮?40
通過(guò)觀察者模式極大地解除了程序間的耦合,雖然主題對(duì)象中仍然依賴了一個(gè)集合類型,但它已經(jīng)被抽象化了,所以耦合度其實(shí)并不算很高,通過(guò)這種方式,我們?cè)诮尤胄碌拈_(kāi)發(fā)者時(shí),只需向主題對(duì)象注冊(cè)即可,若是不想接入了,也可以注銷該開(kāi)發(fā)者。
事件監(jiān)聽(tīng)機(jī)制
了解觀察者模式之后,我們進(jìn)入本篇文章的重心,事件監(jiān)聽(tīng)機(jī)制。
在該模型中,有三個(gè)非常重要的概念:
事件 事件源 事件監(jiān)聽(tīng)器
其具體流程是:用戶操作(比如點(diǎn)擊)導(dǎo)致事件觸發(fā),前提是事件監(jiān)聽(tīng)器已經(jīng)被注冊(cè)好了,事件觸發(fā)后會(huì)生成事件對(duì)象,此時(shí)事件對(duì)象會(huì)作為參數(shù)傳遞給事件監(jiān)聽(tīng)器,監(jiān)聽(tīng)器調(diào)用對(duì)應(yīng)的方法進(jìn)行處理。
在這里事件源就是主題對(duì)象,而事件監(jiān)聽(tīng)器就是觀察者,當(dāng)事件源發(fā)生變化時(shí),主題對(duì)象就會(huì)通知所有的觀察者處理數(shù)據(jù),那么接下來(lái)我們就來(lái)實(shí)現(xiàn)一下。首先創(chuàng)建事件接口:
public interface Event {
// 事件回調(diào)
void callback();
}
然后創(chuàng)建具體實(shí)現(xiàn):
public class ValueEvent implements Event {
// 事件三要素:事件源、事件發(fā)生事件、事件消息
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);
}
}
創(chuàng)建監(jiān)聽(tīng)器接口:
public interface EventListener {
// 觸發(fā)事件
void triggerEvent(Event event);
}
實(shí)現(xiàn)監(jiān)聽(tīng)器:
public class ValueChangeListener implements EventListener {
@Override
public void triggerEvent(Event event) {
// 調(diào)用事件回調(diào)方法
event.callback();
}
}
最后編寫(xiě)事件源接口:
public interface EventSource {
// 注冊(cè)監(jiān)聽(tīng)器
void addListener(EventListener listener);
// 通知所有監(jiān)聽(tīng)器
void notifyListener();
}
實(shí)現(xiàn)事件源接口:
public class ValueSource implements EventSource {
// 管理所有監(jiān)聽(tīng)器
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("更新數(shù)據(jù):" + msg);
}
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
notifyListener();
}
}
編寫(xiě)測(cè)試代碼:
public class Main {
public static void main(String[] args) {
ValueSource source = new ValueSource();
source.addListener(new ValueChangeListener());
source.setMsg("50");
}
}
運(yùn)行結(jié)果:
ValueEvent{source=com.wwj.spring.guanchazhe.click.ValueSource@1d81eb93, when=2021-05-22T13:19:26.806, msg='更新數(shù)據(jù):50'}
我們來(lái)仔細(xì)分析一下這個(gè)過(guò)程,首先我們創(chuàng)建了一個(gè)事件源:
ValueSource source = new ValueSource();
它相當(dāng)于觀察者模式中的主題對(duì)象,也就是被觀察者,當(dāng)被觀察者數(shù)據(jù)發(fā)生變化時(shí),通知所有監(jiān)聽(tīng)器進(jìn)行處理,所以我們?yōu)槠渥?cè)了一個(gè)監(jiān)聽(tīng)器:
source.addListener(new ValueChangeListener());
此時(shí)我們修改事件源的數(shù)據(jù):
source.setMsg("50");
就會(huì)執(zhí)行setMsg方法:
public void setMsg(String msg) {
this.msg = msg;
notifyListener();
}
該方法又調(diào)用了notifyListener方法,通知所有監(jiān)聽(tīng)器處理:
@Override
public void notifyListener() {
for (EventListener listener : listeners) {
ValueEvent event = new ValueEvent();
event.setSource(this);
event.setWhen(LocalDateTime.now());
event.setMsg("更新數(shù)據(jù):" + msg);
listener.triggerEvent(event);
}
}
在該方法中,首先需要?jiǎng)?chuàng)建事件,并設(shè)置事件源,也就是當(dāng)前對(duì)象,設(shè)置事件發(fā)生時(shí)間和消息,最后調(diào)用監(jiān)聽(tīng)器的事件處理方法:
@Override
public void triggerEvent(Event event) {
// 調(diào)用事件回調(diào)方法
event.callback();
}
該方法又調(diào)用了事件的回調(diào)方法:
@Override
public void callback() {
System.out.println(this);
}
事件回調(diào)方法就輸出了當(dāng)前對(duì)象,以上就是整個(gè)事件監(jiān)聽(tīng)機(jī)制的流程。
總結(jié)

最后,我們通過(guò)這張圖,再總結(jié)一下事件監(jiān)聽(tīng)的整個(gè)流程:
首先創(chuàng)建事件源,并為其注冊(cè)事件 當(dāng)調(diào)用setMsg方法修改事件源中的數(shù)據(jù)時(shí),會(huì)調(diào)用notifyListener方法通知所有監(jiān)聽(tīng)器 在notifyListener方法中會(huì)遍歷所有的監(jiān)聽(tīng)器,創(chuàng)建事件對(duì)象,并作為參數(shù)傳入監(jiān)聽(tīng)器的事件處理方法(triggerEvent) 監(jiān)聽(tīng)器的triggerEvent方法會(huì)調(diào)用事件的回調(diào)方法(callback) 回調(diào)方法用于編寫(xiě)具體的處理邏輯,比如輸出內(nèi)容給用戶反饋
本文作者:汪偉俊 為Java技術(shù)迷專欄作者 投稿,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載。

往 期 推 薦 1、Intellij IDEA這樣 配置注釋模板,讓你瞬間高出一個(gè)逼格! 2、吊炸天的 Docker 圖形化工具 Portainer,必須推薦給你! 3、最牛逼的 Java 日志框架,性能無(wú)敵,橫掃所有對(duì)手! 4、把Redis當(dāng)作隊(duì)列來(lái)用,真的合適嗎? 5、驚呆了,Spring Boot居然這么耗內(nèi)存!你知道嗎? 6、全網(wǎng)最全 Java 日志框架適配方案!還有誰(shuí)不會(huì)? 7、Spring中毒太深,離開(kāi)Spring我居然連最基本的接口都不會(huì)寫(xiě)了

點(diǎn)分享

點(diǎn)收藏

點(diǎn)點(diǎn)贊

點(diǎn)在看
