1、我所理解的回调
在查看内部类相关知识点的资料时,总是看到两个关键字:闭包和回调。闭包大概能明白,算是一种程序结构,差不多就是能够访问外部变量的某种“域”,在Java看来也就是内部类了。而回调的话,总是很懵懂,在前端用AJAX知道有这么个东西,但理解不深刻。现在看来,回调大概就是把引用交给别人,由别人在适当的时候调用该引用(这里的引用在Java中往往是对象,在JS中是函数,毕竟JS中函数可以作为对象传递)。你调用别人,即主动调用;别人反过来调用你,就是回调。
在网上四处看了些大概的说法,摘录一些自己比较能够理解的说法:
- 一般写程序是你调用系统的API,如果把关系反过来,你写一个函数,让系统调用你的函数,那就是回调了,那个被系统调用的函数就是回调函数。(https://www.zhihu.com/question/19801131)
- 其实就是传一段代码给某个方法A,然后方法A可以按照自己的需要在适当的时候执行这段传进来的代码。所有的回调应该都是这么个逻辑。(http://www.cnblogs.com/heshuchao/p/5376298.html)
- 编程上来说,一般使用一个库或类时,是你主动调用人家的API,这个叫Call,有的时候这样不能满足需要,需要你注册(注入)你自己的程序(比如一个对象),然后让人家在合适的时候来调用你,这叫Callback。设计模式中的Observer就是例子(http://blog.csdn.net/yu422560654/article/details/7001797)
为什么在闭包的概念里总是提到回调,这是因为Java的闭包中往往要将内部类的引用返回,如 Bar getBar() :
public class Foo {
//成员变量
private int local = 0;
//内部类
class Bar {
public int func() {
local++;
System.out.println(local);
return local;
}
}
//返回一个内部类的引用
public Bar getBar() {
return new Bar();
}
}
内部类的引用交管给别人,由别人在适当的时候调用,这不就是“回调”了嘛。
看一个小小的例子,下例用于打印输出某个方法执行的耗时,通过定义接口的方式实现回调:
public interface Callback {
//执行回调
void execute();
}
public class Tool {
/**
* 测试方法执行的耗时
*
* @param callback 回调方法
*/
public static void timeConsume(Callback callback) {
long start = System.currentTimeMillis();
callback.execute();
long end = System.currentTimeMillis();
System.out.println("[time consume]:" + (end - start) + "ms");
}
}
public class Test {
public static void main(String[] args) {
Tool.timeConsume(new Callback() {
@Override
//填写你需要测试的方法内容,这里简单写个数字计算的例子
public void execute() {
int result = 0;
for (int i = 0; i < 100000; i++) {
result += i;
}
System.out.println(result);
}
});
}
}
在Test中可以看到,直接调用,传入一个匿名内部类实现方法来完成回调,这实际上和JS中传入函数作为变量已经很相似了。你可能要说,JS中传入的函数变量可以是闭包,那么在Java中也很简单,在某个类中写好固定的内部类并写个返回内部类引用的方法,在此处调用timeConsume()时将该引用传入,就和JS中传入函数变量的形式相同了。
2、面向接口回调
另外,还有一点需要提醒的是,在诸多回调的使用中,都是采用的面向接口编程,让某个类实现该接口,然后传入该接口实现类。那么问题来了,为什么不直接传入对象本身的引用?把自己完全暴露给别人,太不安全。
假设现在有类Boss,领导有查看所有人工资viewAllSalary,发工资paySalary等;还有一个员工类Employee。好了,现在Boss交代给Employee某件事,要求其完成之后报告给老板,这就是回调了:
- 如果是面向接口编程,老板要实现TellMeInfo接口,然后实现接口中doThingsWithInfo
- 回调,那得把自己的引用给员工才行,那么以TellMeInfo的实现类的形式给员工就行了
- 员工拿到了Boss的引用,但是因为是面向接口,所以只能执行doThingsWithInfo方法
- 如果我们直接传入对象本身的引用,老板直接写好某个方法doThingsWithInfo
- Boss要求员工完成工作后,调用这个doThingsWithInfo方法
- 员工拿到的是对象本身的引用,拿到一看,卧槽,惊呆了,可做的事情太多了
- 有了这个完整引用,不就可以调用ViewAllSalary查看其他同事的薪资,甚至还能paySalary给自己多发钱
- 员工富裕了,老板的公司倒闭了,老板没弄明白自己错在哪里
下面来看上面场景的模拟代码,先看面向接口编程:
//回调接口
public interface TellMeInfo {
void doThingsWithInfo(String result);
}
//领导
public class Boss implements TellMeInfo{
public void viewAllSalary() {
//输出所有人的工资表
}
public void paySalary(Employee employee, long salary) {
//给某员工发放薪水
}
@Override
public void doThingsWithInfo(String result) {
System.out.println("boss do other things according to the result:" + result);
}
}
//员工
public class Employee {
public String work() {
String result = "balabala";
return result;
}
public void workAndCallback(TellMeInfo boss) {
String result = work();
boss.doThingsWithInfo(result);
}
}
//测试类:领导让员工做完某事后报告给他,然后他才能根据事情结果去处理其他事情
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
Employee employee = new Employee();
employee.workAndCallback(boss);
}
}
那么现在看下如果直接把完整引用给员工:
//领导
public class Boss {
public void viewAllSalary() {
//输出所有人的工资表
}
public void paySalary(Employee employee, long salary) {
//给某员工发放薪水
}
public void doThingsWithInfo(String result) {
System.out.println("boss do other things according to the result:" + result);
}
}
//员工
public class Employee {
public String work() {
String result = "balabala";
return result;
}
public void workAndCallback(Boss boss) {
String result = work();
boss.doThingsWithInfo(result);
//好像还可以利用这个引用做点其他的事情
//先看下其他同事的工资,哇,情敌小明的工资竟然这么高,不开心
boss.viewAllSalary();
//没办法,赶紧给自己多发点钱,这样可以甩小明好几条街,开心
boss.paySalary(this, 999999);
}
}
//测试类不变,老板没看出什么端倪
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
Employee employee = new Employee();
employee.workAndCallback(boss);
}
}
2、回调的方式
- 同步回调,即阻塞,调用方要等待对方执行完成才返回
- 异步回调,即通过异步消息进行通知
- 回调,即双向(类似两个齿轮的咬合),“被调用的接口”被调用时也会调用“对方的接口”
实际上我们用得最多的,还是异步回调。
2.1 同步回调
张老头准备泡茶喝,泡茶之前要先烧水。张老头把灶台点上火,把水壶放上,然后盯着水壶一直等,水开了,张老头用烧开的水,开心地泡起了茶。然后张老头喝好茶,就开始看书了。
public interface Callback {
void execute();
}
public class Elder implements Callback{
private String name;
public Elder(String name) {
this.name = name;
}
public void readBook() {
System.out.println(this.name + " is reading a book.");
}
public void drinkTea() {
System.out.println(this.name + " can drink the tea right now.");
}
@Override
public void execute() {
drinkTea();
}
}
public class Kettle {
public void boilWater(final Callback callback) {
System.out.println("Boiling start");
int time = 0;
for (int i = 0; i < 60 * 10; i++) {
time += 1000;
}
System.out.println("Boiling the water costs " + time + "ms.");
System.out.println("The water is boiling.");
callback.execute();
}
}
public class Test {
public static void main(String[] args) {
Elder elder = new Elder("Zhang");
Kettle kettle = new Kettle();
kettle.boilWater(elder);
elder.readBook();
}
}
//输出
Boiling start
Boiling the water costs 600000ms.
The water is boiling.
Zhang can drink the tea right now.
Zhang is reading a book.
2.2 异步回调
还是张老头烧水喝茶的例子,他发现自己傻等着水开有点不明智,烧水可要10min呢,完全可以在这段时间先去看会儿书。等水烧开了水壶响了,再去泡茶喝,时间就利用起来了。(其他类都不变,Kettle类的boilWater方法作为线程开启,即异步)
public interface Callback {
void execute();
}
public class Elder implements Callback{
private String name;
public Elder(String name) {
this.name = name;
}
public void readBook() {
System.out.println(this.name + " is reading a book.");
}
public void drinkTea() {
System.out.println(this.name + " can drink the tea right now.");
}
@Override
public void execute() {
drinkTea();
}
}
public class Kettle {
public void boilWater(final Callback callback) {
System.out.println("Boiling start");
//开启线程,异步烧水
new Thread(new Runnable() {
@Override
public void run() {
int time = 0;
for (int i = 0; i < 60 * 10; i++) {
time += 1000;
}
System.out.println("Boiling the water costs " + time + "ms.");
System.out.println("The water is boiling.");
callback.execute();
}
}).start();
}
}
public class Test {
public static void main(String[] args) {
Elder elder = new Elder("Zhang");
Kettle kettle = new Kettle();
kettle.boilWater(elder);
elder.readBook();
}
}
//输出
Boiling start
Zhang is reading a book.
Boiling the water costs 600000ms.
The water is boiling.
Zhang can drink the tea right now.
2.3 回调(双向)
“被调用的接口”被调用时也会调用“对方的接口”,这种情况就不适合我们的张老头出场了,双向回调多用于反复依赖对方的数据进行运算的时候,A系统要调用B系统的某个方法b(),但是这个b()方法中某个参数又需要A系统提供,于是需要反过来再调用A系统的某个方法a()提供参数,才能完整执行b()。
那么我为什么不在A系统运算好了参数,在调用B系统的b()方法时候直接以方法参数的形式传递呢?因为你不知道b()中如何使用这个参数,或者说根据条件不同甚至不会使用到这个参数。
如果这个参数的运算比较消耗资源,你不论对方使用与否都先弄出来,一股脑子塞给对方。这跟对方需要用到参数的时候,再调用你进行计算,哪个更节约资源呢?答案显而易见了。
这就跟工厂出货一样,不管市场卖不卖得掉,先生产出来,万一市场没有需求,压根没人买,这批货就烂掉了。但是如果是市场给工厂发了需求订单,工厂再进行相应生产,再出货,那效果就截然不同了。
看一个简单的例子,随机生成某个随机百分比的字符串(A实例调用了B中某个方法,而这个方法需要数据又反过来又调用了A中某个方法):
public interface Callback {
public double takeRandom();
}
public class A implements Callback{
private B b = new B();
@Override
public double takeRandom() {
System.out.println(this + " executing the method takeRandom()");
return Math.random();
}
public void printRandomPercent() {
System.out.println(this + " executing the method printRandomPercent()");
System.out.println("start");
//A类实例的函数调用B类实例的方法
b.doPercent(this);
}
}
public class B {
public void doPercent(Callback action) {
System.out.println(this + " executing the method doPercent()");
double param = action.takeRandom();
DecimalFormat decimalFormat = new DecimalFormat("0.00");
String result = decimalFormat.format(param * 100) + "%";
System.out.println("the calculate-result is " + result);
}
}
public class Test {
public static void main(String[] args) {
A a = new A();
a.printRandomPercent();
}
}
//输出
callback.A@186db54 executing the method printRandomPercent()
start
callback.B@a97b0b executing the method doPercent()
callback.A@186db54 executing the method takeRandom()
the calculate-result is 7.43%