本文由“言念小文”原创,转载请说明文章出处

一、前言

什么是回调?回调如何使用?如何优雅的使用?
本文将首先详解回调的原理,然后介绍回调的基本使用方法,最后介绍基于回调的“观察者模式”实现,演示
如何优化回调使用方法。

二、什么是回调

案例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基于回调的观察者模式详解的更多相关文章

  1. Java网络编程和NIO详解9:基于NIO的网络编程框架Netty

    Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...

  2. “全栈2019”Java第一百一十三章:什么是回调?回调应用场景详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  3. Java网络编程和NIO详解6:Linux epoll实现原理详解

    Java网络编程和NIO详解6:Linux epoll实现原理详解 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO h ...

  4. Java网络编程和NIO详解3:IO模型与Java网络编程模型

    Java网络编程和NIO详解3:IO模型与Java网络编程模型 基本概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32 ...

  5. Elasticsearch java api 基本搜索部分详解

    文档是结合几个博客整理出来的,内容大部分为转载内容.在使用过程中,对一些疑问点进行了整理与解析. Elasticsearch java api 基本搜索部分详解 ElasticSearch 常用的查询 ...

  6. (转)Java并发包基石-AQS详解

    背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...

  7. Java Spring cron表达式使用详解

    Java Spring cron表达式使用详解   By:授客 QQ:1033553122 语法格式 Seconds Minutes Hours DayofMonth Month DayofWeek ...

  8. Java网络编程和NIO详解开篇:Java网络编程基础

    Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为 ...

  9. java.util.logging.Logger使用详解 (转)

    http://lavasoft.blog.51cto.com/62575/184492/ ************************************************* java. ...

随机推荐

  1. UnicodeDecodeError: 'gbk' codec can't decode byte 0xb0 in position 279: illegal multibyte sequence

    with open(r'E:\yy\mysql.txt') as wk: print(wk.readlines()) Traceback (most recent call last): File & ...

  2. 数据分析--pandas的基本使用

    一.pandas概述 1.pandas是一个强大的Python数据分析的工具包,是基于NumPy构建的. 2.pandas的主要功能 具备对其功能的数据结构DataFrame.Series 集成时间序 ...

  3. 三大特征提取器(RNN/CNN/Transformer)

    目录 三大特征提取器 - RNN.CNN和Transformer 简介 循环神经网络RNN 传统RNN 长短期记忆网络(LSTM) 卷积神经网络CNN NLP界CNN模型的进化史 Transforme ...

  4. java+maven+jenkins+svn构建

    操作参照:https://blog.csdn.net/qq_34977342/article/details/82346915 1.创建一个自由风格的项目,起名字 2.设置构建项目最大保存数量,与天数 ...

  5. Hyper-V安装CentOS 8问题

    CentOS 8 已经发布很长时间了,作为一直折腾Linux虚拟机的一员怎么少的了我. 环境&准备工作 系统:Win 10 pro 19H1 虚拟机:Hyper-V ISO:CentOS 8 ...

  6. scalikejdbc 学习笔记(2)

    使用scalikejdbc config (src\main\resources) # MySQL(dev) dev.db.default.driver="com.mysql.jdbc.Dr ...

  7. Kafka 学习笔记之 ZooKeeper作用

    Kafka使用ZooKeeper 配置管理 Leader Election 服务发现 首先进入ZooKeeper客户端: ls / 可以看到有以下节点: 查看Topic 配置信息:体现了ZooKeep ...

  8. .Net Core删除ClientApp目录,重新生成报错解决办法

    因为在老的项目上做修改,需要删除单独的spa目录,就把ClientApp删掉了.但是重新生成报错,在VS2017界面上也没找到在什么地方配置.最后发现在csproj上里面可以去掉spa的配置 < ...

  9. HashMap 取数算法

    Map,百度翻译给我的解释是映射,在Java编程中,它是存储键值对(key-value)的一种容器,也是Java程序员常用的对象.这篇博客介绍下HashMap的实现:java是面向对象编程语言,jdk ...

  10. Github | 吴恩达新书《Machine Learning Yearning》完整中文版开源

    最近开源了周志华老师的西瓜书<机器学习>纯手推笔记: 博士笔记 | 周志华<机器学习>手推笔记第一章思维导图 [博士笔记 | 周志华<机器学习>手推笔记第二章&qu ...