聊一聊Java中的事件监听机制

Java技术迷

共 22046字,需浏览 45分钟

 ·

2021-05-28 18:29

汪伟俊 作者

Java技术迷 | 出品


相信大家都学过Java中的GUI,不知道你们对GUI中的事件机制有没有产生过好奇心,当我们点击按钮时,就可以触发对应的点击事件,这一过程究竟是如何实现的呢?本篇文章我们就来聊一聊Java中的事件监听机制。

在了解事件监听机制之前,我们先来学习一个设计模式——观察者模式,事件监听机制的原理就是它。

场景设置

假设现在有一个需求,你正在运营一个有关天气的接口,要求是可以将天气信息推送出去,前提是接入了该接口的开发者才能收到天气信息,该如何实现呢?

首先我们来创建一个类:

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方法用于更新数据(该方法是由其它类调用的)。

继续创建一个类:

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方法实现数据同步,下面我们就来试一试:

public class Main {

    public static void main(String[] args) {
        PushWeather pushWeather = new PushWeather();
        WeatherDataInterface wdi = new WeatherDataInterface(pushWeather);
        wdi.updateWeatherData(102030);
        System.out.println("更新天气数据");
        wdi.updateWeatherData(203040);
    }
}

运行结果:

温度:10 湿度:20 气压:30 
更新天气数据
温度:20 湿度:30 气压:40 

这种实现方式是有很大弊端的,因为如果又有一个第三方开发者要接入你的接口,那么修改的代码将会非常多,不信来看看,首先创建第三方开发者:

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

然后需要修改的是我们的天气数据接口:

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方法,让其也能更新百度的数据,测试代码:

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(102030);
        System.out.println("更新天气数据");
        wdi.updateWeatherData(203040);
    }
}

运行结果:

温度:10 湿度:20 气压:30 
百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30 
更新天气数据
温度:20 湿度:30 气压:40 
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40 

观察者模式

观察者模式,又被称为发布——订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当该主题对象发生数据变化时,会通知所有的观察者对象更新数据。很显然,在刚才的案例中,第三方开发者就是观察者模式中的观察者,而天气数据接口就是主题对象,当天气数据接口发生变化时,就会通知那些依赖于天气接口的观察者去更新自己的数据,所以刚才的案例是非常适合使用观察者模式来进行改造的,那怎么实现呢?

观察者模式中有几个非常重要的概念:

  1. Subject:抽象主题,它是用于抽象观察者的,因为主题对象需要管理所有依赖于它的观察者,所以必须对观察者抽象,才能实现统一的管理,提供接口注册和注销观察者
  2. ConcreteSubject:具体主题,它用于具体实现主题对象,它会将有关状态存入具体的观察者对象,在具体主题数据发生变化时,会给所有已经注册的观察者发送通知
  3. Observer:抽象观察者,它定义了一个接口,用于对观察者进行抽象
  4. ConcreteObserver:具体观察者,实现抽象观察者接口,以便在得到主题对象的通知时更新自身数据

现在我们就来改造刚才的案例,首先创建抽象主题:

public interface Subject {

    // 注册观察者对象
    void register(Observer observer);

    // 移除观察者对象
    void remove(Observer observer);

    // 通知所有观察者更新数据
    void notify(int temperature, int humidity, int airPressure);
}

然后创建抽象观察者:

public interface Observer {

    // 更新天气数据
    void update(int temperature, int humidity, int airPressure);
}

接着具体实现主题:

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

最后就是创建具体的观察者,也就是第三方开发者:

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

编写测试代码:

public class Main {

    public static void main(String[] args) {
        Baidu baidu = new Baidu();
        WeatherDataSubject subject = new WeatherDataSubject();
        subject.register(baidu);
        subject.notify(102030);
        System.out.println("更新天气数据");
        subject.notify(203040);
    }
}

运行结果:

百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30 
更新天气数据
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40 

现在若是想接入新的第三方开发者,那就变得非常简单了,首先创建新的开发者:

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

然后修改测试代码即可:

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(102030);
        System.out.println("更新天气数据");
        subject.notify(203040);
    }
}

运行结果:

百度接入————温度:10 百度接入————湿度:20 百度接入————气压:30 
阿里巴巴接入————温度:10 阿里巴巴接入————湿度:20 阿里巴巴接入————气压:30 
更新天气数据
百度接入————温度:20 百度接入————湿度:30 百度接入————气压:40 
阿里巴巴接入————温度:20 阿里巴巴接入————湿度:30 阿里巴巴接入————气压:40

通过观察者模式极大地解除了程序间的耦合,虽然主题对象中仍然依赖了一个集合类型,但它已经被抽象化了,所以耦合度其实并不算很高,通过这种方式,我们在接入新的开发者时,只需向主题对象注册即可,若是不想接入了,也可以注销该开发者。

事件监听机制

了解观察者模式之后,我们进入本篇文章的重心,事件监听机制。

在该模型中,有三个非常重要的概念:

  1. 事件
  2. 事件源
  3. 事件监听器

其具体流程是:用户操作(比如点击)导致事件触发,前提是事件监听器已经被注册好了,事件触发后会生成事件对象,此时事件对象会作为参数传递给事件监听器,监听器调用对应的方法进行处理。

在这里事件源就是主题对象,而事件监听器就是观察者,当事件源发生变化时,主题对象就会通知所有的观察者处理数据,那么接下来我们就来实现一下。首先创建事件接口:

public interface Event {

    // 事件回调
    void callback();
}

然后创建具体实现:

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

创建监听器接口:

public interface EventListener {

    // 触发事件
    void triggerEvent(Event event);
}

实现监听器:

public class ValueChangeListener implements EventListener {

    @Override
    public void triggerEvent(Event event) {
        // 调用事件回调方法
        event.callback();
    }
}

最后编写事件源接口:

public interface EventSource {

    // 注册监听器
    void addListener(EventListener listener);

    // 通知所有监听器
    void notifyListener();
}

实现事件源接口:

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

编写测试代码:

public class Main {

    public static void main(String[] args) {
        ValueSource source = new ValueSource();
        source.addListener(new ValueChangeListener());
        source.setMsg("50");
    }
}

运行结果:

ValueEvent{source=com.wwj.spring.guanchazhe.click.ValueSource@1d81eb93, when=2021-05-22T13:19:26.806, msg='更新数据:50'}

我们来仔细分析一下这个过程,首先我们创建了一个事件源:

ValueSource source = new ValueSource();

它相当于观察者模式中的主题对象,也就是被观察者,当被观察者数据发生变化时,通知所有监听器进行处理,所以我们为其注册了一个监听器:

source.addListener(new ValueChangeListener());

此时我们修改事件源的数据:

source.setMsg("50");

就会执行setMsg方法:

public void setMsg(String msg) {
    this.msg = msg;
    notifyListener();
}

该方法又调用了notifyListener方法,通知所有监听器处理:

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

在该方法中,首先需要创建事件,并设置事件源,也就是当前对象,设置事件发生时间和消息,最后调用监听器的事件处理方法:

@Override
public void triggerEvent(Event event) {
    // 调用事件回调方法
    event.callback();
}

该方法又调用了事件的回调方法:

@Override
public void callback() {
    System.out.println(this);
}

事件回调方法就输出了当前对象,以上就是整个事件监听机制的流程。

总结

最后,我们通过这张图,再总结一下事件监听的整个流程:

  1. 首先创建事件源,并为其注册事件
  2. 当调用setMsg方法修改事件源中的数据时,会调用notifyListener方法通知所有监听器
  3. 在notifyListener方法中会遍历所有的监听器,创建事件对象,并作为参数传入监听器的事件处理方法(triggerEvent)
  4. 监听器的triggerEvent方法会调用事件的回调方法(callback)
  5. 回调方法用于编写具体的处理逻辑,比如输出内容给用户反馈
好了,以上就是本篇文章的全部内容了。我觉得写的还算通俗易懂,希望对你入门有帮助吧!

本文作者:汪伟俊 为Java技术迷专栏作者 投稿,未经允许请勿转载。

1、Intellij IDEA这样 配置注释模板,让你瞬间高出一个逼格!
2、吊炸天的 Docker 图形化工具 Portainer,必须推荐给你!
3、最牛逼的 Java 日志框架,性能无敌,横扫所有对手!
4、把Redis当作队列来用,真的合适吗?
5、惊呆了,Spring Boot居然这么耗内存!你知道吗?
6、全网最全 Java 日志框架适配方案!还有谁不会?
7、Spring中毒太深,离开Spring我居然连最基本的接口都不会写了

点分享

点收藏

点点赞

点在看

浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报