如何理解回调?

是什么?怎么理解?

参考:http://www.cnblogs.com/xrq730/p/6424471.html#commentform

 

一、模块间的调用

回调是模块间的一种调用机制(不限于java)。

借用R大的回答:

“回调”(callback)这个概念本身跟Java没啥关系,是通用的。

这个概念就是说:我写了这个答案,并且说我有空的时候再来更新(声明接受回调的事件)。下面评论区就有很多同学跑来说“如果有更新请告知”(注册回调)。然后我更新了回答,并且在评论区吼了声“我更新了”(事件发生了,调用回调)。然后之前“注册回调”的同学们就会收到通知说有更新了。

很多学习Java的同学喜欢说设计模式。嗯。那宽泛地说,其实什么strategy、template、observer、visitor模式全部都是回调的不同应用。

简单来说就是本来可以写死在一起的代码给拆开来,把让其中一坨保持原有的流程,并在流程中挖出一些空,让另一坨代码作为参数传进来在流程中合适的地方被调用。这个“合适的地方”可以是同步的也可以是异步的。

个人认为,想要了解回调的概念,需要先了解模块间调用的几种常见方法。

现有需求如下:老师出题让学生完成,学生完成后老师读出学生的答案。如何使用“同步调用”、“异步调用”、“回调”的方式模拟此过程?

(1)同步调用

1

同步调用是最基本并且最简单的一种调用方式。A类的方法a()直接调用B类的方法b(),等待b()方法执行完毕,a()方法才继续向下执行。

实现如下:

Teacher.java

定义了一个setProblem方法,直接调用student对象中的solveTheProblem方法。

Student.java

通过延时3s模拟学生的解题过程。

Main.java

运行结果为:

在此实现下,xie解题(执行solveTheProblem方法)需要3s,在这3s内,Teacher必须等待xie解题完毕(等待solveTheProblem方法执行完毕),不能处理其他工作(被阻塞)。直到xie给出结果后,Teacher才能读出xie的答案。

这种解决方案存在两个问题:

1.Teacher的setProblem方法会被Student的solveTheProblem方法阻塞

如果solveTheProblem方法非常耗时(比如100s),Teacher线程将被阻塞100s,在此期间无法进行任何操作,浪费线程资源。

(这间接告诉我们,如果方法耗时严重,就不适合使用同步调用。)

2.没有考虑存在多个学生的情况

在存在多个学生的情况下,如果使用同步方法,Teacher只能改写成这样:

相当于学生一个一个地做题,老师一个一个地读出学生的答案,Teacher线程阻塞的时间变成了所有学生解题时间的总和。如果存在很多学生,那么setProblem方法将运行相当长的一段时间,同样浪费线程资源。

同步调用无法处理“存在多个学生”的情况,一般使用异步调用的方式解决。

(2)异步调用

1

异步调用是为了解决同步调用可能出现阻塞,导致整个流程卡住而产生的一种调用方式。A类的方法a()通过新起线程的方式调用B类的方法b(),a()方法可以继续往下执行。这样无论方法b()执行时间多久,都不会阻塞住方法a()。

但是,由于方法a()不等待方法b()的执行,在方法a()需要方法b()执行结果的情况下,必须通过一定的方式获取方法b()的执行结果。

从常识出发,如果存在多个学生,老师应该同时给所有学生出题,所有学生同时开始解题。因为学生的解题速度存在差异(解题的时间有快有慢),所以采取先来先得的做法,哪个学生先做完,老师就读出其答案,直到读出所有学生的答案为止。

实现代码如下:

Teacher.java

因为强调的是“完成”,所以我使用了CompletionService(使用Future + Callable也可以实现)。

Student.java

Student类实现了Callable接口,运算过程也放入了call方法中。

Main.java

运行结果为:

这种解决方案看起来没毛病,我们继续拓展一下需求:

老师出题让学生完成,学生思考之后先告诉老师解题思路,然后完成解题,最后老师读出学生的答案。

有些程序员可能会说:“这好办,我们另写一个结果类,把解题思路和答案包装到一起,返回就好了。”

这种解决方法有两个问题:

1.不能解决顺序问题

要注意,“先告诉老师解题思路”、“最后老师读出学生的答案”意味着必须先告诉老师解题思路,再告诉老师答案,是存在先后顺序的两个动作。

也就是说,如果继续使用异步调用的方式,必须返回两次结果。那么问题来了,一个方法怎么返回两次结果呢?(不使用回调实现不了此场景)

2.如果异步线程返回的对象过大,可能存在问题

如果某个学生的解题思路极为复杂,一次性说给老师听,连老师都受不了(如果异步线程返回的对象过大,主线程可能出现问题),所以必须分次说给老师听(分次返回数据进行处理)。

(我没遇过这种情景。问题是,就算对象不大,也可能需要分次处理)

异步调用难以解决上面的问题。我们可以尝试使用回调的方式。

(3)回调

1

回调的思想是:

  • A类的a()方法调用B类的b()方法。
  • B类的b()方法执行完毕后主动调用类A的callback()方法。

如果a()方法使用同步的方式调用b()方法,就是同步回调。如果使用异步的方式,就是异步回调。

1.同步回调

使用同步回调的方式解决第一个问题(单个学生的情况):

CallBack.java

先写一个回调接口,定义一个回调方法。

Teacher.java

从结果来看,“老师获取学生的答案”(Teacher类中的setProblem方法使用同步/异步的方式调用Student类中的resolveQuestion方法)等同于“学生主动告诉老师答案”(Student类中resolveQuestion方法反过来调用Teacher类中的tellAnswer方法)。

Student.java

Student类主动调用Teacher类中的tellAnswer方法实现回调。

Main.java

运行结果为:

2.异步回调

使用同步回调的方式解决第一个问题(多个学生的情况):

CallBack.java

先写一个回调接口,定义一个回调方法。

Teacher.java

因为异步线程主动调用回调方法,而不是主线程获取异步线程执行的结果,所以不需要使用Future/Callable,只需要开启普通的线程即可。

Student.java

最重要的一步是获取回调的对象。

Main.java

运行结果为:

可以看见,“well done kids!”被提前输出了,这说明Teacher类中没有发生阻塞,可以一直往下执行。

3.如果需要返回思考过程,只需要稍微改动

CallBack.java

在回调接口中增加一个需要回调的方法。

Teacher.java

实例化此回调方法。

Student.java

在异步线程中调用此回调方法。

运行结果为:

二、总结

适当使用回调可以让程序更加灵活,个人认为不需要刻意使用。具体情况具体分析,总会有适合使用回调的场景。

发表评论

电子邮件地址不会被公开。 必填项已用*标注