Java中的回调

在使用Java编程开发这块,各种开源框架、Android开发以及平时我们很少碰到的GUI编程等等,会经常碰到回调这种技术。如果你之前就已经学过C/C++,那肯定学过函数指针,或多或少都会接触到回调。在C/C++中,回调一般都是用函数指针这个特性实现的,而其他语言的实现手法就不太一样了。

到底什么是回调?

我的经历

我看过很多关于描述回调的文章,仅有几篇文章是真正让读者理解到什么是回调,而大多数文章,有的是简单地说了下什么是回调,然而这些内容并没清楚地说明回调的本质是什么,但让读者从某个语言的角度上理解个表面还是可以的,然后再列出一些让大家看了之后对回调理解更含糊的代码;反之,有的文章大篇幅的描述回调,举出各种例子,具体例子抽象例子都有,再加上各种术语,就不说读者看起来头很疼,估计写的人自己再回顾也会被绕晕。

真正的问题所在

先不从语言上进行考虑,而是从本质上进行探究。为什么回调解释起来很让人懊恼?有时虽然是清楚了,但是写起代码来总是感到困惑,或者代码都写错了,自己并不知道。大多数情况下都是因为“回调”这一术语对你产生误导的作用。这一点,在stackoverflow或知乎上有些关于回调的话题讨论中也提到过:“回调(callback)”这一个术语会让人对回调的理解产生迷惑。

这个术语会经常让我们单纯地认为回调就像打电话一样只是你我之间双方的互相呼应,可能就因为这样,所以有很多关于回调的文章都会出现“If you call me, I will call you back.”或者是“If you call me, I will call back.”。但这样来说明是有一定的误导性的,只能说双方相互呼应只是回调的一种特殊情况罢了。

回调的本质

首先来看看维基里回调的定义

在计算机编程中,一小段可执行的代码作为一个参数传递到另一段代码,并且它将会在某个合适的时间回调(执行),这就是回调(callback)

或许你会注意到定义里所提到的:某个合适的时间,其实这个指的是某些“事件”的发生,而这个“事件”可能是条件判定、函数调用、按钮点击事件或者是计时器计时等等。

这是对回调广义上的描述,也是最本质的描述。但是我们在讨论关于回调的时候都经常将其称为回调函数。这是因为在我们使用各种编程语言再加以运用回调技术的时候,那段进行回调的代码基本上都是函数。不同的语言,实现回调的方式也不一样,可以用以下几种技术实现回调:子程序、lambda表达式、块、函数指针。

那现在我们就来用自己的话来描述一下回调函数:

  • 回调函数(callback function)所指的是一个函数作为参数传递到另一个函数,并且它将在某些“事件”(条件判定、函数调用、按钮点击事件或者是计时器计时等)触发后被调用执行,以完成我们指定的任务

这时,想必大家都对什么是回调,什么是回调函数都有了一个印象比较深刻的初步认识。这几句简洁易懂的描述的就是它们的本质,但是你是否发现这些定义都没有描述到回调的“回”这一个字的意思?

上面这个图来源于维基百科。这个图很形象地描述了回调中“回”字的意思。

我们通常都在使用别人已经编写好的库为自己所要做的应用进行编程。如上图所示,回调函数和应用的主函数处在同一个抽象层(高层抽象),所要做的应用有什么样的需求,我们就编写完成对应需求的回调函数传递给底层抽象,因此,回调函数和应用的主函数处于同一个抽象层这点是很自然而然的。

在高层抽象向底层抽象传递回调函数的前提下,这个图所描述的过程:

  • 高层抽象调用底层抽象
  • 底层抽象再过头来用高层抽象

综上所述:回调的“回”的意思就是程序执行流程从底层回到高层。

回调实例演示

本人现阶段专攻Java,所以这里要演示的例子还是使用Java来实现。

分析

Java没有C/C++里面的函数指针,那实现回调只能是另辟蹊径。在Java中,基本上都是使用接口或许附带内部类这一特性实现回调的。

现在还是来看一个现实中打电话的例子:

老板和员工的情景:老板给员工分配一个任务,并且老板告诉员工:“这个任务完成后,你就打电话给我,并且向我说明一下解决方案”。分配完任务后,老板外出或者做其他事去了。在某个时刻,员工终于把那个任务完成了,他就打电话给老板报告一下任务完成的情况以及解决方案。

在动手写代码之前,我们必须把这个情景中几个关键的动作搞清楚:

  • 老板分配任务给员工
  • 老板告诉员工任务完成后打电话给他并说明解决方案
  • 任务完成后,员工给老板打电话并说明解决方案

按照上面关于回调函数的描述,从这几个动作我们可以得出:

  • 员工打电话给老板这个动作就是回调方法(callback method)
  • 任务完成这个情况就是员工打电话给老板这个回调方法被调用之前所触发的事件
  • 老板告诉员工任务完成后要打电话给他这个动作就是注册回调方法(register callback method)

代码

代码的具体实现细节我们暂且不关心,我们先用代码来模拟那个情景的具体过程,再逐步深入实现其中的细枝末节。

老板和员工整个情景过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Company {
public static final String EASY_TASK = "这是一个简单的任务";
public static final String DIFFICULT_TASK = "这是一个棘手的任务";
public static final String VERY_DIFFICULT_TASK = "这是一个非常棘手的任务";

public static void main(String[] args) {
// 老板
Boss boss = new Boss();
// 打工仔
Worker worker = new Worker();
// 老板给属下员工分配任务
boss.assign(worker, DIFFICULT_TASK);
// 派遣完任务后, 老板做其他事情去了
boss.doOtherThings();
}
}

这个过程用代码模拟后看起来是不是很简单、很清晰?至于任务我就用常量作简化处理了,毕竟我们此处最关心的并不是这些。

再来看老板Boss的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Boss {
private String telNumber = "1360****459";

public void assign(final Worker worker, final String task) {
System.out.println("---老板开始分配任务---");
new Thread() {
@Override
public void run() {
// 员工接受老板分配的任务, 并且接受老板吩咐任务完成后要做的事
worker.accept(task, new OnTaskComplete() {
@Override
public void callBack(String solution) {
System.out.println("打电话给xxx: " + telNumber);
System.out.println("并且报告解决方案: " + solution);
}
});
}
}.start();
System.out.println("----老板分配任务完毕----");
}

public void doOtherThings() {
System.out.println("-----老板去做其他事情了-----");
}
}

此处,我们主要关注的肯定是assign分配任务的方法,这个方法接收2个参数,分别是员工worker和任务task

既然老板分配任务给员工,员工worker肯定有一个方法接受任务并解决的。
员工着手任务时,不能让老板瞎等吧?所以要运用异步调用对这种情况进行模拟。这里便开辟一个新线程来调用员工worker接受任务的方法accpet

方法accept同样是接收2个参数,任务task参数是必要的,而另一个参数是一个实现OnTaskComplete回调接口的匿名内部类对象。
  在编程的角度上,这种行为就是上面分析所说的注册回调函数(register callback function)
  在情景的角度上,这就是对“老板告诉员工任务完成后要做什么什么”这个动作的模拟。

而匿名内部类里的callBack方法就是回调方法(callback method)
  该方法的具体实现就如同之前的情景:“老板给员工交代过,任务完成后打电话给老板并向他说明一下解决方案”
  这里并没有具体的解决方案,因为这个解决方案需要员工在完成任务后回传才会有。

看完了老板Boss,接着肯定要看看打工仔Worker啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Worker {
public void accept(String task, OnTaskComplete followUpWork) {
if (Company.EASY_TASK.equals(task)) {
solve(task, 1, 3);
if (followUpWork != null) {
followUpWork.callBack("解决方案A");
}
} else if (Company.DIFFICULT_TASK.equals(task)) {
solve(task, 4, 10);
if (followUpWork != null) {
followUpWork.callBack("解决方案B");
}
} else {
System.out.println("该任务中的需求过于复杂,需要大家开会讨论");
}
}

private void solve(String task, int bestTime, int worstTime) {
int actualTime = (int) (Math.random() * (worstTime - bestTime + 1)) + bestTime;
try {
Thread.sleep(actualTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("解决任务( " + task + " )实际耗时: " + actualTime);
}
}

Worker中有2个方法,其中方法solve只是模拟员工耗时解决任务。而员工接受任务的accept方法才是重点。

方法accept接收2个参数,一个是任务task,另一个则是OnTaskComplete接口形式的参数followUpWork,用于调用回调方法callBack
方法accpet很好地模拟了员工解决任务并且在之后打电话并报告解决方案这个动作。
  员工根据任务的难易,估算一下解决任务的时间,并开始着手解决,这个耗时动作使用方法solve进行模拟。
  当solve执行完毕后,即任务完成了,员工需要打电话给老板说明解决方案,这里就使用followUpWork回调callBack回传解决方案。

最后,是回调接口的定义。这个就没什么可说了。接口这东西,只要你搞清楚需求,就很好去定义它了。

1
2
3
public interface OnTaskComplete {
void callBack(String solution);
}

虽然这个情景整个过程乍看之下是比较简单,代码过程简洁清晰之余,还符合文章之前一直强调的回调或回调函数的定义,不过在我用Java代码合理地把它模拟出来还是费了不少功夫的(毕竟之前我也没怎么研究过回调,讲道理嘛)。其实我看过的某些博客文章解释回调,文字上是挺清晰的,但是一旦看代码我就卡住了,总觉得代码对情景模拟得不够自然,大多数都是由于命名不怎么好而导致我有这种感觉(代码的命名很重要,命名不好,别人或自己看起来就纠结了)。

而使用回调的好处,就如《Thinking In Java》里所说的:回调的价值在于它的灵活性——可以在运行时动态地决定需要调用什么方法

以上就是我对回调的一些总结和见解,希望这些能够对大家有所帮助。有些地方可能有误,如果你有不同的看法,可以提出来,3Q。