最近接触了一些前端 JavaScript 方面的东西,觉得 JS 最方便的地方便是各种回调函数,无论是 Ajax 还是与用户的前端交互,都是通过回调函数的形式实现的。于是乎来到 Java 中搜索了一下有关回调的相关内容,结果发现还真的有。查了很多文章,总算是弄清楚了 Java 中回调的使用方法,然而大部分讲解都没有说到其实际的用途,于是便再次进行记录,也是给自己一个备忘。
什么是回调
接触过 JS 或者做过服务端接口的人应该都会对回调十分熟悉。按我的理解,假设有 A 和 B 两个模块,A 调用 B 来执行某一个功能,这个功能执行可能会花费一些时间,而且 A 希望在 B 执行完之后得到通知,那么 B 执行完相关功能之后再对 A 进行通知的这个过程,就叫做回调。通俗来讲就是甲乙两个人,甲需要一把梯子来换灯泡,让乙去拿来,乙把梯子拿来以后需要告诉甲:梯子已经到了,可以换灯泡了。
在 JS 中的回调十分便捷,只要将收到回调后需要执行的方法传入调用的方法中即可,这种回调方式的调用不仅限于 JS,Ruby 中也有类似的语法,然而对于 Java 来说,实现回调虽然有些麻烦,但是仍然有着解决办法。
Java 中的回调
在 Java 中实现一个回调不像脚本语言那样简洁优雅,但是会有一种更加规范的感觉。
此处还是用之前提到的的 A 和 B,生命这两个模块为两个类,主要归结为以下步骤:
- 定义一个 Callback 接口,用来指明回调时候需要执行的方法。
- 让 A 类实现 callback 接口,并且 A 类需要持有一个 B 的实例对象 b(因为只有持有才能调用)。
- 让 B 类中实现一个参数带有 callback 接口参数的方法 f(Callback callback, Object args)。
- A 类的实例对象 a 调用 b.f,并且将 a 自身作为 callback 参数传入。
- b.f 此时可以通过 callback 调用 A 中定义的 Callback 接口中的问题。
乍看下来感觉整个人都不好了,乱七八糟的,其实结合代码的话会看着很简单。此处以刚才说到的甲让乙搬梯子,然后甲用梯子换灯泡的情景,写一个 Demo。
首先是定义接口类,该接口声明了甲需要完成的换灯泡工作:
package io.muxin;
/**
* 回调接口定义,
* Created by mxfang on 16/6/29.
*/
public interface Callback {
/**
* 换灯泡过程
*/
void changeBulb();
}
其次先定义乙的类,乙的类是提供一个搬梯子的功能,并且在搬好以后通知让他搬梯子的人:
package io.muxin;
/**
* 这是乙的类
* Created by mxfang on 16/6/29.
*/
public class Yi {
/**
* 去搬梯子, 搬好以后通过回调通知甲
* @param callback 持有甲的回调
*/
public void getLadder(Callback callback) {
System.out.println("乙: 开始去搬梯子");
try {
// 搬梯子需要时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("乙: 梯子搬来了, 通知甲");
// 回调甲
callback.changeBulb();
}
}
然后定义甲,甲有两个功能,第一是尝试去换灯泡(不确定有没有梯子),发现没梯子的话让乙去搬梯子,第二个功能是正式换灯泡,这个由乙来通知甲进行操作:
package io.muxin;
/**
* 这是甲的类
* Created by mxfang on 16/6/29.
*/
public class Jia implements Callback{
private Yi yi;
/**
* 构造函数, 让甲能持有一个乙的实例
* @param yi 乙的实例
*/
public Jia(Yi yi) {
this.yi = yi;
}
/**
* 尝试换灯泡
*/
public void tryChangeBulb() {
System.out.println("甲: 没有梯子, 让乙先去搬梯子");
yi.getLadder(this);
}
@Override
public void changeBulb() {
System.out.println("甲: 梯子到位, 开始换灯泡啦");
}
}
最后是测试类:
package io.muxin;
public class Main {
public static void main(String[] args) {
// 首先要有一个乙
Yi yi = new Yi();
// 然后甲需要持有乙
Jia jia = new Jia(yi);
// 让甲尝试换灯泡
jia.tryChangeBulb();
}
}
运行结果:
甲: 没有梯子, 让乙先去搬梯子
乙: 开始去搬梯子
乙: 梯子搬来了, 通知甲
甲: 梯子到位, 开始换灯泡啦
Process finished with exit code 0
可以看到整个过程被完整的执行了下来,重点在于乙做一个耗时工作之后能够反向调用一个甲的方法。
这里可能有人会问为何会使用接口,其实效果等价于 B 类也持有了一个 A 类的实例,然后对 A 进行调用。我个人认为此处使用接口的意义在于可以将 B 类做的事情通用化,比如在数据库操作中,A 类可能要执行一个写入操作,然而 B 类需要执行对数据库的连接和打开操作,那么 B 类可能是 MySQL、Redis、Mongodb 等不同数据库的操作实例。
第二个感觉可能的情况是 A 和 B 模块分别属于两个人或者团队编写,通过提前制定好的接口,根据时序来实现功能。
拓展
写在这篇文章的最后,Java 的回调并不仅仅用于异步的操作。大家可以跟我一起回想起来 Android 开发的时候,设置一个按钮的动作,setOnClickListener 这个方法,需要传入一个实现了 OnClickListener 接口的类来进行设定,这就是一个同步回调的过程,我之前也没有发觉这是一个回调,直到查阅了相关资料后才恍然大悟。
这次对回调的学习暂且到这里,也算是对回调有了个基本了解,它的思路被用在了 Java 中很多很多地方,比如 Android 和 Hibernate 中都大量使用回调,以后有时间去阅读代码的时候再深入的来学习吧。