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) {//给某员工发放薪水}@Overridepublic 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.");}@Overridepublic 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 startBoiling 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.");}@Overridepublic void execute() {drinkTea();}}
public class Kettle {public void boilWater(final Callback callback) {System.out.println("Boiling start");//开启线程,异步烧水new Thread(new Runnable() {@Overridepublic 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 startZhang 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();@Overridepublic 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()startcallback.B@a97b0b executing the method doPercent()callback.A@186db54 executing the method takeRandom()the calculate-result is 7.43%