不知从什么时候开始,回调函数这个词进入了我的脑海。但是一直以来我都害怕着它,每次听到别人说回调这个词就会觉得这个人肯定很牛,然后自愧不如。最后我也决定去研究一下什么是回调。于是我到网上看了一些文章,可以看出来作者都很想让读者看懂,也会举一些挺不错的例子,并且贴出代码,看完以后觉得自己似乎懂了。但是一段时间后当我再次接触回调函数的时候,却又会犯糊涂,因为感觉这和我在文章中看到的回调不一样,然后我又会去看网上的文章,然后再犯糊涂 . . .
双向的回调
在我看的文章里面,有的作者会用A和B的形式来讲解,作者会说:如果A调用了B,然后B调用了A,这就是回调,这里的A和B通常指不同的类,代码大致如下:
接口:
package com.lg.callback.test;
public interface Interface {
void handle();
}
类A:
package com.lg.callback.test; public class A implements Interface { @Override public void handle() { System.out.println("A.handle()"); } public void callB(B b) { System.out.println("类A调用类B"); b.callA(this); } }
类B:package com.lg.callback.test; public class B { public void callA(Interface i) { System.out.println("类B调用类" + i.getClass().getSimpleName()); i.handle(); } }
客户端测试:
package com.lg.callback.test;
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.callB(b);
}
}
输出结果:
类A调用类B
类B调用类A
A.handle()
当我看完这种文章以后,就会认定这就是回调。我会觉得回调是一个你来我往的,双向的过程,我调用了你,然后你又反过来调用了我,这才是回调。
看着像单向的回调
但是问题来了,在看《java编程思想》和Spring源码的时候,所说的回调不是这样子的。似乎只要一个方法定义了一个接口参数,然后传入这个接口的实现,在方法体中通过传入的接口实现调用这个接口实现的方法就算是回调了。代码大致如下:
接口:
package com.lg.callback; public interface Interface { void handle(); }
类A:
类B:package com.lg.callback; public class A implements Interface { @Override public void handle() { System.out.println("A.handle()"); } }
客户端测试:package com.lg.callback; public class B { public void execute(Interface i) { System.out.println("类B调用类" + i.getClass().getSimpleName()); i.handle(); } }
测试结果:package com.lg.callback; public class Main { public static void main(String[] args) { A a = new A(); B b = new B(); System.out.println("客户端调用类B"); b.execute(a); } }
客户端调用类B 类B调用类A A.handle() 在上面的例子中,客户端类调用了类B,而类B调用了类A,也就是说调用过程是:客户端-->B-->A,这明显是单向调用,为什么会叫回调呢?但是我又对《java编程思想》和Spring源码深信不疑,所以我更加愿意相信这才是回调,但是应该怎样说服自己呢?通常我会采用“百度”的方式来说服自己。开发环境--类A:C语言中的回调
当我百度“回调函数”的时候,第一条来自百度百科,定义如下:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
记得当年学习C语言的时候确实学过函数指针,但是当时并不知道回调函数,如果按照百度百科中的说法,那么下面的代码就属于回调函数:
执行结果:#include <stdio.h> void fun1(); void fun2(); typedef void (*funPoint)(); // 定义函数指针 void callback(funPoint fp); // 声明一个以函数指针为参数的方法 int main() { // 将具体函数传入回调函数,并且调用回调函数 callback(fun1); callback(fun2); return 0; } // 具体函数fun1 void fun1() { printf("%s\n", "fun1 is called"); } //具体函数fun2 void fun2() { printf("%s\n", "fun2 is called"); } //定义回调函数 void callback(funPoint fp) { // 在函数体中调用函数指针指向的函数 fp(); }
fun1 is called
fun2 is called
但是这跟java中的回调有什么关系呢?估计有些学过C和java的读者已经有所察觉,C语言中函数指针非常类似java中的接口。函数指针指向一个具体函数,而这些具体函数可以根据用户的需要来自行定义;同样接口声明了一个(或多个)方法,用户可以定义一个类来实现这个接口的所有方法,然后根据自己的需要来定义这些方法。在C语言的回调中,是将函数指针作为回调函数的参数,然后在回调函数体中调用函数指针指向的具体函数,最后调用回调函数时,具体函数就会被调用。同样java中的回调函数也是将接口作为回调函数的参数,然后在回调函数的函数体中通过接口参数调用接口声明的方法,最后向回调函数中传入接口的实现类并调用回调函数,就可以调用实现类中自己实现的方法。
这么看来java回调不一定是“A调用了B,B也要调用A”这种双向的形式。为了更加坚信这种观点,我特意爬到墙上并翻了出去(维基百科在大陆已经上不了了),看看维基百科有没有这方面的定义,结果发现,看了这么多的文章还不如人家的几行定义。维基百科对回调的定义如下:
在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
回调的使用场景
那么,回调究竟是单向的还是双向的呢?首先可以确定的是,如果针对类来说,那回调不必是双向的(从上面的“看着像单向的回调”这个小节的例子可以看出来);但是如果限定一个场景的话,那么回调应该是双向的。这个场景中有两个角色,一个角色是已有的代码库,这个代码库只提供给给程序员使用,而不能被修改;另一个角色是程序员当前的开发环境,开发环境中的代码是可以让程序员随时改动的。就拿上面的“看着像单向的回调”这个小节的例子来说,如果把类B和接口Interface放到代码库中,而把类A和客户端类放到开发环境中,那么调用过程:客户端-->B-->A 可以看成是双向的。过程如下:
客户端-->B ==》开发环境-->代码库
B-->A ==》代码库-->开发环境
这样一来,类之间的单向调用变成了开发环境和代码库之间的双向调用。现在我们假设code_library包中的类都是不能修改的代码库,developer包中的类是可以修改和添加的开发环境,完整代码如下:
代码库--接口:
代码库--类B:package code_library; public interface Interface { void handle(); }
package code_library; public class B { public void execute(Interface i) { System.out.println("代码库调用开发环境 -- 类B调用类" + i.getClass().getSimpleName()); i.handle(); } }
开发环境--客户端:package developer; import code_library.Interface; public class A implements Interface { @Override public void handle() { System.out.println("A.handle()"); } }
package developer;
import code_library.B;
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
System.out.println("开发环境调用代码库 -- 客户端类调用类B");
b.execute(a);
}
}
执行结果:开发环境调用代码库 -- 客户端类调用类B 代码库调用开发环境 -- 类B调用类A A.handle()
小小的提醒,说一下网上一些关于回调的文章,有时候为了更形象并且贴近生活地给读者描述回调,会在讲解回调的时候使用回调+异步调用的例子(当然作者通常都会说明例子里面包含了异步调用,也不排除一些作者忘了说明的)。例如:张三叫李四帮自己做一件事情,然后张三叫李四做完这件事情后打电话通知自己。这个例子里面当张三叫李四做事之后,张三不可能一直盯着电话等李四的回电,而要接着做自己的事情,所以李四做事应该放在一个新的线程里面,当然李四打电话通知张三也是在这个新的线程里,这样一来就涉及了异步调用。所以这种例子虽然也包含回调,但是这已经不是最纯粹,最简单的回调,所以不能把回调误解为回调+异步调用。
最后说明一下,文章中所有的例子都可以算是回调,最简单和典型的回调是“看着像单向的回调”小节中的例子。