7.9 回调与事件机制

7.9.1 回调(callback)

回调这个词听起来很深奥,不过这个概念并不是Java所特有的,在使用WIN32 API进行Windows编程时,就会用到回调。下面我们来看看Windows编程中的回调,如代码7.18所示。

img
img

在C语言中,回调其实就是一个函数指针,当我们把这个指针告诉操作系统时,操作系统会根据需要来调用这个函数。

在C#中,也有回调这个概念,不过我们在使用.Net进行编程时更多地使用了事件这个概念。与C语言类似,C#中的回调也是一个类似函数指针的类型——代理(delegate)。

于是我们可以得出一个结论:回调就是一段代码,在需要的时候被调用。

在C语言中,我们可以使用函数指针来指向这段回调代码,在C#中我们可以使用代理(delegate)来指向这段回调代码,那么Java既没有指针也没有代理,它是如何做到这一点的呢?答案是接口。

我们看代码7.19。

img

程序运行的结果为:

img

我们在Operation接口中声明了一个方法(也就是回调方法),然后让MyImplement类来实现了这个接口,接下来我们定义了一个Caller类来负责调用这个回调方法。在main方法中,我们向Caller类的对象传入了一个MyImplement对象(在回调概念中,可以说是注册回调方法)。

在这个例子中,我们没有使用内部类,而且花费了好大力气就是为了得到一个简单的结果,这看起来没有什么意义。实际上,我们只是通过这个例子来说明Java中回调机制的工作模式。

接下来我们使用一个比较复杂的例子来说明在回调中使用内部类的优势,如代码7.20所示。

img
img

程序运行结果为:

img

在上面的例子中,我们同时使用了内部类和匿名内部类来实现ClickHandler接口,它们对接口方法onClick的实现就是后面要调用的回调代码。由于内部类和匿名内部类都可以访问外部类中的所有成员,于是我们在处理bt2对象的click事件的回调方法中(即匿名内部类的onClick方法),修改MyWindow的bt1对象的名字。图7-3给出了整个例子的UML结构图。

从这张类图中我们可以看出,Button类只跟ClickHandler回调接口打交道,而MyWindow中的两个内部类:Btn1ClickHandler和匿名内部类,则实现了ClickHandler接口,它们生成的对象向上类型为Clickhandler接口类型,并通过Button类的registerHandler方法传递给MyWindow中的两个Button对象(btn1和btn2)。当MyWindow类的run方法调用两个Button对象的click方法时,这两个对象就分别调用Btn1ClickHandler类和匿名内部类对象的onClick方法。

img

图7-3 代码7.20的UML类图

虽说这个过程看起来比较复杂,但是这种设计方式就是Java图形界面编程中事件处理的核心。

7.9.2 事件(event)

在了解了回调之后,我们来研究一下事件。在Java的图形界面API——Swing中,会大量接触到事件这个概念,你会更进一步地发现内部类在处理事件时得天独厚的优势。

事件机制的核心是回调,但是它还包含了一些其他的内容:事件类(Event)、事件监听器(Event Listener)和产生事件的对象。代码7.21简单地描述了一个事件系统的工作方式,这个事件系统的原型来自Java的Swing。

img
img

程序的运行结果为:

img

这段代码展现了一个事件系统的基本工作方式,代码模拟了按钮点击的过程。下面我们来阐述一下事件系统的工作原理。

首先,我们可以把“点击按钮”看成一个事件,于是就有了事件这个概念(Event)。

其次,与这个事件相关的一些信息(事件的发起者、事件的发生时间、事件的其他描述等)应该被记录下来,我们可以称这些信息为事件的参数(argument)。

然后,应该有“人”对这个事件感兴趣,其一直在等待这个事件的发生,这个“人”我们称之为事件监听者(Event Listener),“人”可以是一个,也可以是多个。

最后,事件系统负责把发生的事件推送给对此事件感兴趣的监听者,并由监听者处理这个事件,对事件做出响应。

有了上述的事件概念之后,理解上面的例子就不是很困难了。当MyButton对象的click方法被调用时(按钮被点击),MyButton类会生成一个ClickEvent对象(产生一个点击事件),并把事件的发起者和事件的发生时间保存在ClickEvent对象中,然后把这个事件对象传递给已经注册到MyButton对象中的ActionListener对象(事件监听器)。注册到MyButton对象的ActionListener对象是一个匿名的内部类对象,它负责处理ClickEvent事件。

我们通过定义Event基类来提供一个基础的事件模型,同时在ActionListener接口的doAction方法中使用Event类型的参数,这样可以保证整个程序的灵活性。