本文由“言念小文”原创,转载请说明文章出处java
1、前言设计模式
什么是回调?回调如何使用?如何优雅的使用?
本文将首先详解回调的原理,而后介绍回调的基本使用方法,最后介绍基于回调的“观察者模式”实现,演示
如何优化回调使用方法。多线程
2、什么是回调并发
案例1
现有一农场须要向气象局订阅天气预报信息。农场向气象局发出订阅请求,气象局接受农场的订阅请求后,
天天都会向农场推送后一天的天气信息。农场天天接受到天气预报信息,将作对应的生产安排,具体安排
以下:若是气温在0~10℃,播种小麦,若是气温在11~15℃播种大豆,若是气温在16~20℃播种棉花,不然
维护农场设备。异步
咱们从“案例1”中能够提取回调的概念。
首先,农场向气象局订阅天气预报信息,气象局会在当天向农场发送第二天的天气预报。这里有两个异步条件:
a.农场订阅天气预报后,气象局不可能当即回复此后每一每天气预报信息;
b.农场并不知道气象局会在前一天具体哪个精确时间点将天气预报发送给本身。
所以“农场-气象局”之间信息传递是异步的。
其次,农场接收到天气预报信息后,才会进行“工做安排”,因为农场不知道天气预报信息返回的精确时间,所以进行
“工做安排”的时机实际是由气象局决定的。天然地,咱们想到将“工做安排”用一个函数(func())来实现,而且该函数的
具体实现由农场(Farm类)来实施,而函数的调用位置及调用时机由气象局(MeteorologicalBureau类)来决定。这就是一个典型的回调场景,
而func()函数被称之为回调函数。下面咱们给出回调的通俗描述:
程序中某一模块A(类/库/其余)中经过一段代码(类/函数)实现某一功能(模块A定义该功能实现的具体细节),但该段代码
执行并不取决于模块A,而是由模块B(类/库/其余)决定,这时一般预先将该段代码的入口地址做为参数传递
给模块B,由模块B在程序的运行期间根据具体状况来选择什么时候何地调用这段代码。这一过程便称做回调。
经过下图直观理解回调函数
3、如何使用回调
咱们经过实现“案例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,该类负责接受天气预报信息的订阅和发布天气预报信息spa
/** * 气象局(天气预报信息发布者) * @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()被
气象局回调后,紧接着便执行了农场的工做安排。
4、回调进阶(基于回调的“观察者模式”实现)
在学会了回调的基本使用方法后,咱们将案例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()方法。
5、结语回调是咱们平常开发工做中使用最为基础最为频繁的技术手段,不管是同步调用仍是异步调用场景(特别是异步调用使用尤为多)有大量应用。若是您也是跟我当初同样是初入行的小白,但愿本文对您有用,另外在java开发中常常用到判null处理,本文代码中常用在判null时,大量使用return处理,我的以为这是一个好习惯,多使用return以减小逻辑判断的嵌套,使代码更容易阅读。