Java基于回调的观察者模式详解
本文由“言念小文”原创,转载请说明文章出处
一、前言
什么是回调?回调如何使用?如何优雅的使用?
本文将首先详解回调的原理,然后介绍回调的基本使用方法,最后介绍基于回调的“观察者模式”实现,演示
如何优化回调使用方法。
二、什么是回调
案例1
现有一农场需要向气象局订阅天气预报信息。农场向气象局发出订阅请求,气象局接受农场的订阅请求后,
每天都会向农场推送后一天的天气信息。农场每天接受到天气预报信息,将做对应的生产安排,具体安排
如下:如果气温在0~10℃,播种小麦,如果气温在11~15℃播种大豆,如果气温在16~20℃播种棉花,否则
维护农场设备。
我们从“案例1”中可以提取回调的概念。
首先,农场向气象局订阅天气预报信息,气象局会在当天向农场发送次日的天气预报。这里有两个异步条件:
a.农场订阅天气预报后,气象局不可能立即回复此后每一天天气预报信息;
b.农场并不知道气象局会在前一天具体哪一个精确时间点将天气预报发送给自己。
因此“农场-气象局”之间信息传递是异步的。
其次,农场接收到天气预报信息后,才会进行“工作安排”,由于农场不知道天气预报信息返回的精确时间,因此进行
“工作安排”的时机实际是由气象局决定的。自然地,我们想到将“工作安排”用一个函数(func())来实现,并且该函数的
具体实现由农场(Farm类)来实施,而函数的调用位置及调用时机由气象局(MeteorologicalBureau类)来决定。这就是一个典型的回调场景,
而func()函数被称之为回调函数。下面我们给出回调的通俗描述:
程序中某一模块A(类/库/其他)中通过一段代码(类/函数)实现某一功能(模块A定义该功能实现的具体细节),但该段代码
执行并不取决于模块A,而是由模块B(类/库/其他)决定,这时通常预先将该段代码的入口地址作为参数传递
给模块B,由模块B在程序的运行期间根据具体情况来选择何时何地调用这段代码。这一过程便称作回调。
通过下图直观理解回调

三、如何使用回调
我们通过实现“案例1”来演示如何使用回调
第一步,创建一个关于气象局的监听接口MeteorologicalBureauListener,该接口中声明气象局相关行为的函数,
这里声明了天气信息发布函数onRelease()。
public interface MeteorologicalBureauListener {
/**
* 天气信息发布
* @param description 天气预报信息描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
void onRelease(String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale);
}
第二步,创建气象局类MeteorologicalBureau,该类负责接受天气预报信息的订阅和发布天气预报信息
/**
* 气象局(天气预报信息发布者)
* @author WenYong
*
*/
public class MeteorologicalBureau { private MeteorologicalBureauListener mListener; /**
* 注册对"气象局"类监听
* @param listener
*/
public void register(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
mListener = listener;
} /**
* 取消注册对"气象局"类监听
* @param listener
*/
public void unregister(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(mListener.equals(listener)){
mListener = null;
}
} /**
* 天气信息预测
*/
public void predict(){
new Thread(new Runnable() { public void run() {
try {
// 模拟耗时操作
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mListener.onRelease("明日渝北区气温8~10℃,风力5~8级", 8, 10, 5, 8);
}
}).start(); } }
第三步,创建农场类,该类订阅天气预报信息,并在接收到天气预报信息后做对应工作安排
/**
* 农场(天气预报信息订阅者)
* @author WenYong
*
*/
public class Farm { private MeteorologicalBureau mBureau;
private MeteorologicalBureauListener mBureauListener; public Farm(MeteorologicalBureau bureau){
mBureau = bureau;
} /**
* 订阅天气信息
*/
public void subscribe(){
System.out.println(TestUtils.getTimeStamp() + "," + "农场订阅天气预报信息");
mBureauListener = new MeteorologicalBureauListener() { public void onRelease(String description, int minTemperature,
int maxTemperature, int minWindscale, int maxWindscale) {
System.out.println(TestUtils.getTimeStamp() + ","
+ "农场接收到天气信息:" + description);
doAfterReceiveWheatherInfo(minTemperature, maxTemperature);
}
};
mBureau.register(mBureauListener);
} /**
* 取消订阅天气信息
*/
public void unsubscribe(){
mBureau.unregister(mBureauListener);
} /**
* 接收到气象局发布的天气信息后,农场做对应的工作安排
* @param minTemperature 明日最低气温
* @param maxTemperature 明日最高气温
*/
private void doAfterReceiveWheatherInfo(int minTemperature, int maxTemperature){
String timeStamp = TestUtils.getTimeStamp();
if(minTemperature >= 0 && minTemperature <= 10){
System.out.println(timeStamp + "," + "农场明日工作安排:播种小麦");
}else if(minTemperature >= 11 && minTemperature <= 15){
System.out.println(timeStamp + "," + "农场明日工作安排:播种大豆");
}else if(minTemperature >= 16 && minTemperature <= 20){
System.out.println(timeStamp + "," + "农场明日工作安排:播种棉花");
}else{
System.out.println(timeStamp + "," + "农场明日工作安排:维护设备");
}
} }
第四步,编写测试类,首先new一个农场对象并订阅天气预报信息,然后气象局调用predict()函数预测天气并发布预报
public class Test {
public static void main(String[] args) {
MeteorologicalBureau bureau = new MeteorologicalBureau();
// 农场订阅天气信息
new Farm(bureau).subscribe();
// 气象局进行天气信息预测
bureau.predict();
}
}
第五步,运行,结果如下:
2019-02-06 21-54-59,农场订阅天气预报信息
2019-02-06 21-55-08,农场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-06 21-55-08,农场明日工作安排:播种小麦
从结果不难看出,农场向气象局订阅天气预报后,9s后气象局向农场发布了天气预报信息,然后农场根据天气预报信息
做出了对应工作安排。
原理分析:
好了,看到了结果之后,我们来分析如何实现回调的。第一步在气象局的监听接口MeteorologicalBureauListener中声明
了天气信息发布接口onRelease()。然后第二步在农场类Farm的天气订阅方法subscribe()中,以内部类对象的方式实现MeteorologicalBureauListener
接口并重写onRelease()方法,然后通过mBureau.register(mBureauListener)将内部类对象传递给气象局对象,这样实际就将Farm对象中实现的onRelease()方法
传递给了气象局对象。从而气象局对象就可以根据具体情况来调用该方法了。再看测试类Test中,首先农场订阅天气信息的过程,就将
Farm对象中定义的接口方法onRelease()传递给了气象局对象,然后气象局对象调用predict(),该方法先模拟耗时9s,然后便执行了onRelease()方法,这样
相当于便将天气信息发布给了Farm对象,由于农场对象事先已定义好接收到天气预报信息后的工作安排doAfterReceiveWheatherInfo(),故而当onRelease()被
气象局回调后,紧接着便执行了农场的工作安排。
四、回调进阶(基于回调的“观察者模式”实现)
在学会了回调的基本使用方法后,我们将案例1稍加修改,增加一个天气预报订阅者
案例2
现有一农场和一机场需要向气象局订阅天气预报信息。农场和机场向气象局发出订阅请求,气象局接受订阅请求后,
每天都会向农场和机场推送后一天的天气信息。农场每天接受到天气预报信息,将做对应的生产安排,具体安排
如下:如果气温在0~10℃,播种小麦,如果气温在11~15℃播种大豆,如果气温在16~20℃播种棉花,否则
维护农场设备;机场接收到天气预报信息,将采取对应的运营管理措施,具体如下:如果风力小于5级,不做预警正常起飞,
如果风力5~8级,预警起飞,如果风力大于8级,暂停起飞。
案例2中看一看出,气象局发布信息是一对多的关系,如下图:

这便是我们开发中经常遇到的观察者模式(设计模式中的观察者模式在此不多做介绍),农场和机场作为“观察者”向气象局订阅天气预报信息,气象局作为信息发布者
每天以一对多的方式,向农场和机场“广播”信息。那么如何通过回调实现”一对多“的信息发布呢?
第一步,创建一个关于气象局的监听接口MeteorologicalBureauListener,该接口中声明气象局相关行为的函数,
这里声明了天气信息发布函数onRelease()。
public interface MeteorologicalBureauListener {
/**
* 天气信息发布
* @param description 天气预报信息描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
void onRelease(String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale);
}
第二步,创建气象局类MeteorologicalBureau,该类负责接受天气预报信息的订阅和发布天气预报信息。需要注意
这里使用了一个并发队列来存储机场和农场传递过来的MeteorologicalBureauListener实现对象的引用。之所以使用ConcurrentLinkedQueue
是为了防止在后面遍历的时候出现多线程问题:遍历的同时被修改,从而导致软件闪退。
/**
* 气象局(天气预报信息发布者)
* @author WenYong
*
*/
public class MeteorologicalBureau { private ConcurrentLinkedQueue<MeteorologicalBureauListener> mListenerQueue; public MeteorologicalBureau(){
mListenerQueue = new ConcurrentLinkedQueue<>();
} /**
* 注册对"气象局"类监听
* @param listener
*/
public void register(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(mListenerQueue.contains(listener)){
return;
}
mListenerQueue.add(listener);
} /**
* 取消注册对"气象局"类监听
* @param listener
*/
public void unregister(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(!mListenerQueue.contains(listener)){
return;
}
mListenerQueue.remove(listener);
} /**
* 天气信息预测
*/
public void predict(){
new Thread(new Runnable() { public void run() {
try {
// 模拟耗时操作
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
}
release(mListenerQueue, "明日渝北区气温8~10℃,风力5~8级", 8, 10, 5, 8);
}
}).start(); } /**
* 天气预报信息发布
* @param queue 监听队列
* @param description 天气预报描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
private void release(ConcurrentLinkedQueue<MeteorologicalBureauListener> queue,
String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale){
if(null == queue || queue.isEmpty()){
return;
}
Iterator<MeteorologicalBureauListener> it = queue.iterator();
while(it.hasNext()){
it.next().onRelease(description, minTemperature,
maxTemperature, minWindscale, maxWindscale);
}
} }
第三步,创建农场类(农场类代码和案例1相同这里不再重复贴出)和机场类
/**
* 机场(天气预报信息订阅者)
* @author WenYong
*
*/
public class Airport { private MeteorologicalBureau mBureau;
private MeteorologicalBureauListener mBureauListener; public Airport(MeteorologicalBureau bureau){
mBureau = bureau;
} /**
* 订阅天气信息
*/
public void subscribe(){
System.out.println(TestUtils.getTimeStamp() + "," + "机场订阅天气预报信息");
mBureauListener = new MeteorologicalBureauListener() { public void onRelease(String description, int minTemperature,
int maxTemperature, int minWindscale, int maxWindscale) {
System.out.println(TestUtils.getTimeStamp() + ","
+ "机场接收到天气信息:" + description);
doAfterReceiveWheatherInfo(minWindscale, maxWindscale);
}
};
mBureau.register(mBureauListener);
} /**
* 取消订阅天气信息
*/
public void unsubscribe(){
mBureau.unregister(mBureauListener);
} /**
* 接收到气象局发布的天气信息后,机场做对应的运营管理措施
* @param minWindscale
* @param maxWindscale
*/
private void doAfterReceiveWheatherInfo(int minWindscale, int maxWindscale){
String timeStamp = TestUtils.getTimeStamp();
if(maxWindscale < 5){
System.out.println(timeStamp + "," + "机场明日运营管理措施:不做预警正常起飞");
}else if(minWindscale >= 5 && maxWindscale <= 8){
System.out.println(timeStamp + "," + "机场明日运营管理措施:预警起飞");
}else{
System.out.println(timeStamp + "," + "机场明日运营管理措施:暂停起飞");
}
} }
第四步,编写测试类,首先分别new一个农场对象和机场对象,并订阅天气预报信息,然后气象局调用predict()函数预测天气并发布预报
public class Test {
public static void main(String[] args) {
MeteorologicalBureau bureau = new MeteorologicalBureau();
// 农场订阅天气信息
new Farm(bureau).subscribe();
// 机场订阅天气信息
new Airport(bureau).subscribe();
// 气象局进行天气信息预测
bureau.predict();
}
}
第五步,运行,结果如下:
2019-02-07 10-35-54,农场订阅天气预报信息
2019-02-07 10-35-54,机场订阅天气预报信息
2019-02-07 10-36-03,农场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-07 10-36-03,农场明日工作安排:播种小麦
2019-02-07 10-36-03,机场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-07 10-36-03,机场明日运营管理措施:预警起飞
从结果可以看出,农场和机场分别向气象局订阅了天气预报信息,9s模拟耗时后,气象局向它们发布了天气预报信息,二者并根据对应
天气信息作了对应工作安排和运营管理。
原理分析:
案例2中回调的实现原理与案例1中相同,在此不再赘述。不同点在于案例2如何实现“一对多”的回调。气象局类MeteorologicalBureau中使用
并发队列mListenerQueue来存储机场和农场传递过来的MeteorologicalBureauListener实现对象的引用,这样气象局就可以调用二者中实现的onRelease()方法。
MeteorologicalBureau中调用私有的release()来对mListenerQueue中对象实现遍历,从而遍历各订阅对象中onRelease()方法。
五、结语
回调是我们日常开发工作中使用最为基础最为频繁的技术手段,不论是同步调用还是异步调用场景(特别是异步调用使用尤其多)有大量应用。
如果您也是跟我当初一样是初入行的小白,希望本文对您有用,另外在java开发中经常用到判null处理,本文代码中经常使用在判null时,大量
使用return处理,个人觉得这是一个好习惯,多使用return以减少逻辑判断的嵌套,使代码更容易阅读。
Java基于回调的观察者模式详解的更多相关文章
- Java网络编程和NIO详解9:基于NIO的网络编程框架Netty
Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...
- “全栈2019”Java第一百一十三章:什么是回调?回调应用场景详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- Java网络编程和NIO详解6:Linux epoll实现原理详解
Java网络编程和NIO详解6:Linux epoll实现原理详解 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO h ...
- Java网络编程和NIO详解3:IO模型与Java网络编程模型
Java网络编程和NIO详解3:IO模型与Java网络编程模型 基本概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32 ...
- Elasticsearch java api 基本搜索部分详解
文档是结合几个博客整理出来的,内容大部分为转载内容.在使用过程中,对一些疑问点进行了整理与解析. Elasticsearch java api 基本搜索部分详解 ElasticSearch 常用的查询 ...
- (转)Java并发包基石-AQS详解
背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...
- Java Spring cron表达式使用详解
Java Spring cron表达式使用详解 By:授客 QQ:1033553122 语法格式 Seconds Minutes Hours DayofMonth Month DayofWeek ...
- Java网络编程和NIO详解开篇:Java网络编程基础
Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为 ...
- java.util.logging.Logger使用详解 (转)
http://lavasoft.blog.51cto.com/62575/184492/ ************************************************* java. ...
随机推荐
- Elastic Static初识(01)
写在前面 Elastic Static 是指由Elasticsearch,Logstash,Kibana,Beats等组件结合起来而构成的一个数据收集,分析,可视化的一个架构.我们经常听说过的ELK就 ...
- opencv边缘检测
人眼怎么识别图像边缘? 比如有一幅图,图里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘.也就是像素的灰度值快速变化的地方. sobel算子 sobel算子是一个离散差分算子. 图像 ...
- idea必备快捷键
ctrl + F: 在当前文件进行文本查找 ctrl + R: 在当前文件进行文本的替换 ctrl + Z: 撤销操作 ctrl + Y:删除光所在的行 或者选中的行 ctrl + D: 复制光标所在 ...
- JAVA 8 新特性Stream初体验
什么是 Stream? Stream(流)是一个来自数据源的元素队列并支持聚合操作 <strong元素队列< strong="">元素是特定类型的对象,形成一个队 ...
- MariaDB简单操作
RHEL7之后操作系统带的数据库都是mariadb,跟mysql一样用 1.安装客户端和服务端 [root@localhost ~]# yum install mariadb mariadb-serv ...
- php使用正则表达式提取字符串中尖括号、小括号、中括号、大括号中的字符串
$str="你好<我>(爱)[北京]{天安门}"; echo f1($str); //返回你好 echo f2($str); //返回我 echo f3($str); ...
- (七十九)c#Winform自定义控件-导航菜单
前提 入行已经7,8年了,一直想做一套漂亮点的自定义控件,于是就有了本系列文章. GitHub:https://github.com/kwwwvagaa/NetWinformControl 码云:ht ...
- IDEA 学习笔记之 Spark/SBT项目开发
Spark/SBT项目开发: 下载Scala SDK 下载SBT 配置IDEA SBT:(如果不配置,就会重新下载SBT, 非常慢,因为以前我已经下过了,所以要配置为过去使用的SBT) 新建立SBT项 ...
- postgres 数据库 citus 集群分片
前言 什么时候需要考虑做数据切分? 1.能不切分尽量不要切分 并不是所有表都需要进行切分,主要还是看数据的增长速度.切分后会在某种程度上提升业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好 ...
- LitePal的存储操作
传统的存储数据方式 其实最传统的存储数据方式肯定是通过SQL语句拼接字符串来进行存储的,不过这种方式有点过于“传统”了,今天我们在这里就不讨论这种情况.实际上,Android专门提供了一种用于存储 ...