事件处理
导航 用户界面 主题: ) |
Java 平台事件模型是 Java 平台上事件驱动编程的基础。
无论你使用什么 编程语言 或 范式,你都有可能遇到一个情况,你的程序必须等待外部事件发生。也许你的程序必须等待一些用户输入,或者也许它必须等待数据通过网络传递。或者可能是其他事情。无论如何,程序必须等待某些超出程序控制范围的事情发生:程序不能使该事件发生。
在这种情况下,有两种通用的选择可以使程序等待外部事件发生。其中第一个称为轮询,这意味着你写一个类似于“当事件未发生时,再次检查”的小循环。轮询非常容易构建,并且非常直观。但它也非常浪费:这意味着程序会占用处理器时间,却什么也没做,只是在等待。对于必须进行大量等待的程序来说,这通常被认为是一个太大的缺点。有很多等待时刻的程序(例如,具有图形用户界面的程序,经常不得不长时间等待用户执行操作),当它们使用另一种机制时,通常会表现得更好:事件驱动编程。
在事件驱动编程中,必须等待的程序只会进入休眠状态。它不再占用处理器时间,甚至可能从内存中卸载,并且通常使计算机能够执行有用的操作。但是程序并没有完全消失;相反,它与计算机或 操作系统 达成了一种协议。一种类似于这样的协议
好的,操作系统先生,既然我必须等待事件发生,我会暂时离开,让您继续做有用的工作。但作为回报,您必须在事件发生时通知我,并让我回来处理它。
事件驱动编程通常对程序的设计有很大的影响。通常,程序需要被分成独立的部分来进行事件驱动编程(一部分用于一般处理,另外一部分或更多部分用于处理发生的事件)。Java 中的事件驱动编程比非事件驱动编程更复杂,但它可以更有效地利用硬件,有时(比如在开发图形用户界面时)将代码分成事件驱动的块实际上非常符合程序的结构。
在本模块中,我们将研究 Java 平台事件驱动编程设施的基础,并研究该基础在整个平台中的一些典型使用示例。
Java 平台上对事件驱动编程的支持最有趣的一点是,实际上没有这样的支持。或者,根据您的观点,平台中有许多不同的独立部分提供了他们自己的事件驱动编程支持。
Java 平台没有提供事件驱动编程的通用实现的原因与平台提供的支持的起源有关。早在 1996 年,Java 编程语言刚刚问世,还在努力立足,并在软件开发中为自己争取一席之地。这种早期开发的一部分集中在软件开发工具上,比如 IDE。当时软件开发的趋势之一是面向用户界面的可重用软件组件:这些组件将某种有趣、可重用功能封装到一个单一包中,可以作为一个整体进行处理,而不是作为一组松散的独立类。Sun Microsystems 试图通过引入他们所谓的 JavaBean 加入组件潮流,这是一个不仅面向 UI 而且可以从 IDE 中轻松配置的软件组件。为了实现这一点,Sun 提出了一个关于 JavaBeans 的大型规范(JavaBeans 规范),主要涉及命名约定(以便从 IDE 中轻松处理这些组件)。但 Sun 同时意识到,以 UI 为中心的组件需要支持一种事件驱动的连接方式,将组件中的事件连接到必须由单个开发人员编写的业务逻辑。因此,JavaBeans 规范还包括一个关于 Java 平台事件模型的小型规范。
当他们开始着手开发这个事件模型时,Sun 的工程师面临着一个选择:试图提出一个庞大的规范来涵盖事件模型所有可能的用途,或者只指定一个抽象的、通用的框架,该框架可以在特定情况下进行扩展以供个人使用。他们选择了后者,因此,无论是喜欢还是厌恶,Java 平台除了这个通用的事件模型框架之外,没有其他通用的事件驱动编程支持。
事件模型框架本身非常简单,由三个类(一个抽象类)和一个接口组成。最重要的是,它包含程序员必须遵守的命名约定。该框架在右侧的图像中进行了描述。
从类和接口的角度来看,框架中最重要的部分是 java.util.EventObject
抽象类和 java.util.EventListener
接口。这两个类型是 Java 平台事件模型规则和约定的核心,具体包括:
- 当事件发生时需要被通知的类称为事件监听器。事件监听器对它感兴趣的每种事件通知类型都有一个不同的方法。
- 事件通知方法声明被分组到不同的类别中。每个类别都由一个事件监听器接口表示,该接口必须扩展
java.util.EventListener
。按照约定,事件监听器接口命名为<事件类别名称>Listener。任何将被通知事件的类都必须至少实现一个监听器接口。 - 与事件发生相关的所有状态都将捕获在一个状态对象中。该对象的类必须是
java.util.EventObject
的子类,并且必须至少记录哪个对象是事件的来源。这样的类称为事件类,按照约定命名为<事件类别名称>Event。 - 通常(但不一定!)一个事件监听器接口会与一个事件类相关联。事件监听器可能具有多个事件通知方法,这些方法接受相同的事件类作为参数。
- 事件通知方法通常(但不一定!)具有传统的签名public void <特定事件>(<事件类别名称>Event evt)。
- 作为事件来源的类必须有一个方法,允许为每种可能的监听器接口类型注册监听器。按照约定,这些方法必须具有签名public void add<事件类别名称>Listener(<事件类别名称>Listener listener)。
- 作为事件来源的类可能有一个方法,允许为每种可能的监听器接口类型取消注册监听器。按照约定,这些方法必须具有签名public void remove<事件类别名称>Listener(<事件类别名称>Listener listener)。
这看起来很多,但是一旦您习惯了就非常简单。请查看左侧的图像,它包含了一个关于如何使用该框架的通用示例。在这个示例中,我们有一个名为 EventSourceClass
的类,它发布有趣的事件。遵循事件模型的规则,这些事件由 InterestingEvent
类表示,该类包含指向 EventSourceClass
对象的引用(source
,继承自 java.util.EventObject
)。
每当发生有趣事件时,EventSourceClass
必须通过调用为此目的存在的通知方法来通知它知道的所有该事件的监听器。所有通知方法(在本示例中只有一个,interestingEventOccurred
)都已按主题分组到一个监听器接口中:InterestingEventListener
,它实现了 java.util.EventListener
,并根据事件模型的约定命名。所有事件监听器类(在本例中只有 InterestingEventListenerImpl
)必须实现此接口。由于 EventSourceClass
必须能够通知所有感兴趣的监听器,因此必须能够注册它们。为此,EventSourceClass
有一个 addInterestingEventListener
方法。由于这是必需的,因此还有一个 removeInterestingEventListener
方法。
从示例中可以清楚地看出,使用事件模型主要涉及遵循命名约定。这乍一看可能有点麻烦,但命名约定的目的是允许自动化工具访问和使用事件模型。事实上,确实存在许多基于这些命名约定的工具、IDE 和框架。
关于事件模型,还有一点需要注意,那就是模型中没有什么。事件模型的设计允许实现者在实现选择方面拥有很大的自由度,这意味着事件模型可以作为各种特定、专用事件处理系统的基础。
除了命名约定和一些基本类和接口之外,事件模型还指定了以下内容:
- 必须能够注册和取消注册监听器。
- 事件源必须通过调用所有已注册监听器上的正确通知方法来发布事件。
- 调用事件通知方法是一个正常的、同步的 Java 调用,该方法必须由调用它的同一个线程执行。
但事件模型并没有指定所有这些如何完成。没有关于哪些类必须是事件源,以及它们如何跟踪已注册的事件监听器的规则。因此,一个类可能发布它自己的事件,或者负责发布与整个对象集合(如整个组件)相关的事件。事件源可能允许在任何时间(即使在处理事件过程中)取消注册监听器,或者可能将其限制在某些时间(这与多线程有关)。
此外,事件模型没有指定它如何嵌入到任何程序中。因此,虽然模型指定调用事件处理方法是一个同步调用,但模型并没有规定事件处理方法不能将任务委派给另一个线程,或者整个事件模型实现必须在应用程序的主线程中运行。事实上,Java 平台的标准用户界面框架(Swing)包含一个事件处理实现,该实现作为桌面应用程序的完整子系统运行,在它自己的线程中运行。
事件通知方法、单播事件处理和事件适配器
[edit | edit source]在上一节中我们提到,事件通知方法通常接受一个参数。这是首选约定,但规范允许在应用程序确实需要的情况下对此规则进行例外。一个典型的例外情况是,当事件通知必须通过网络以非 Java 方式发送到远程系统时,比如 CORBA 标准。在这种情况下,需要多个参数,而事件模型允许这样做。但是,作为一般规则,通知方法的正确格式是
代码段 1.1:简单通知方法
public void specificEventDescription(Event_type evt)
|
我们之前提到的另一件事是,作为一般规则,事件模型允许许多事件监听器为同一个事件向同一个事件源注册。在这种情况下,事件源必须将所有相关事件广播给所有已注册的监听器。但是,事件模型规范再次允许对该规则进行例外。如果从设计角度来看这是必要的,您可能会将事件源限制为注册单个监听器;这称为 *单播事件监听器注册*。当使用单播注册时,如果注册了太多监听器,则注册方法必须声明为抛出 java.util.TooManyListenersException
异常
代码段 1.2:监听器注册
public void add<Event_type>Listener(<Event_type>Listener listener) throws java.util.TooManyListenersException
|
最后,规范允许另一个扩展:事件适配器。事件适配器是事件监听器接口的实现,可以插入到事件源和实际的事件监听器类之间。这是通过使用常规注册方法将适配器注册到事件源对象来完成的。适配器用于为事件处理机制添加额外的功能,例如事件对象的路由、事件过滤或在实际事件处理类处理之前丰富事件对象。
一个简单的例子
[edit | edit source]在上一节中,我们探索了 Java 平台事件模型框架的深度(如果有的话)。如果您像大多数人一样,您会发现理论文本比模型的实际使用更令人困惑。当然比解释什么更令人困惑,实际上是一个非常简单的框架。
为了澄清一切,让我们检查一个基于事件模型框架的简单示例。假设我们要编写一个程序,该程序读取用户在命令行输入的数字流并以某种方式处理该流。例如,通过跟踪数字的运行总和,并在流完全读取后生成该总和。
当然,我们可以使用 main() 方法中的循环非常简单地实现该程序。但相反,让我们更具创造性。假设我们想将程序整齐地划分为类,每个类都有自己的责任(就像我们在适当的面向对象设计中应该做的那样)。并且让我们假设我们不仅希望计算所有读取的数字的总和,而且希望对同一个数字流执行任意数量的计算。实际上,应该能够相对轻松地添加新的计算,而无需影响任何先前存在的代码。
如果我们分析这些需求,我们会得出结论,我们在程序中有很多不同的责任
使用事件模型框架允许我们干净地分离两个主要职责,并为我们提供了我们正在寻找的灵活性。如果我们在单个类中实现读取数字流的逻辑,并将读取单个数字视为事件,则事件模型允许我们将该事件(以及该数字)广播到我们喜欢的任意数量的流处理器。读取数字流的类将充当程序的事件源,每个流处理器将是一个监听器。由于每个监听器都是一个独立的类,并且可以向流读取器注册(或不注册),这意味着我们的模型允许我们拥有多个独立的流处理,我们可以将其添加到其中,而不会影响读取流的代码或任何预先存在的流处理器。
事件模型表示与事件关联的任何状态都应包含在表示事件的类中。这对我们来说很完美;我们可以实现一个简单的事件类,它将记录从命令行读取的数字。然后,每个监听器都可以根据需要处理此数字。
对于我们有趣的事件集,让我们保持简单:让我们将自己限制为读取新数字和到达流的末尾。通过这种选择,我们得到了以下示例应用程序的设计
在接下来的部分中,我们将介绍此示例的实现。
示例基础知识
[edit | edit source]让我们从基础开始。根据事件模型规则,我们必须定义一个事件类来封装我们有趣的事件。我们应该将这个类命名为 *something-something*Event。让我们使用 NumberReadEvent
,因为这将是我们感兴趣的。根据模型规则,该类应该封装属于事件发生的任何状态。在我们的例子中,这就是从流中读取的数字。我们的事件类必须继承自 java.util.EventObject
。因此,总而言之,以下类就是我们所需要的
代码清单 1.1:NumberReadEvent。
package org.wikibooks.en.javaprogramming.example;
import java.util.EventObject;
public class NumberReadEvent extends EventObject {
private double number;
public NumberReadEvent(Object source, Double number) {
super(source);
this.number = number;
}
public double getNumber() {
return number;
}
}
|
接下来,我们必须定义一个监听器接口。此接口必须定义有趣事件的方法,并且必须扩展 java.util.EventListener
。我们之前说过我们有趣的事件是“读取数字”和“到达流的末尾”,所以我们来试试
代码清单 1.2:NumberReadListener。
package org.wikibooks.en.javaprogramming.example;
import java.util.EventListener;
public interface NumberReadListener extends EventListener {
public void numberRead(NumberReadEvent numberReadEvent);
public void numberStreamTerminated(NumberReadEvent numberReadEvent);
}
|
实际上,numberStreamTerminated
方法有点奇怪,因为它实际上不是“读取数字”事件。在实际程序中,您可能希望以不同的方式执行此操作。但让我们在这个例子中保持简单。
事件监听器实现
[edit | edit source]因此,在定义了监听器接口之后,我们需要一个或多个实现(实际的监听器类)。至少我们需要一个用来跟踪读取的数字的运行总和。当然,我们可以添加任意数量的。但现在让我们坚持使用一个。显然,此类必须实现我们的 NumberReadListener
接口。保持运行总和是随着事件的到来将数字添加到字段的问题。并且我们希望在到达流的末尾时报告总和;由于我们知道何时发生(即调用 numberStreamTerminated
方法),一个简单的 println
语句就足够了
代码清单 1.3:NumberReadListenerImpl。
package org.wikibooks.en.javaprogramming.example;
public class NumberReadListenerImpl implements NumberReadListener {
double totalSoFar = 0D;
@Override
public void numberRead(NumberReadEvent numberReadEvent) {
totalSoFar += numberReadEvent.getNumber();
}
@Override
public void numberStreamTerminated(NumberReadEvent numberReadEvent) {
System.out.println("Sum of the number stream: " + totalSoFar);
}
}
|
那么,这段代码好吗?不。它很糟糕,非常糟糕,最重要的是不线程安全。但它适合我们的示例。
事件源
[edit | edit source]这里事情变得有趣:事件源类。这个地方很有趣,因为我们必须在这里放置代码来读取数字流,代码向所有监听器发送事件,以及代码来 *管理* 监听器(添加和删除它们以及跟踪它们)。
让我们从考虑跟踪监听器开始。通常这很棘手,因为您必须考虑各种多线程问题。但在这个例子中我们保持简单,所以让我们坚持使用一个简单的 java.util.Set
的监听器。我们可以在构造函数中初始化它
代码段 1.1:构造函数
private Set<NumberReadListener> listeners;
public NumberReader() {
listeners = new HashSet<NumberReadListener>();
}
|
这种选择使实现添加和删除监听器变得非常容易
代码段 1.2:注册/注销
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
|
在这个示例中,我们实际上不会使用 remove 方法 - 但请记住,模型说它必须存在。
这种简单选择的另一个优点是,通知所有监听器也很容易。我们只需假设所有监听器都在集中,然后对它们进行迭代即可。由于通知方法是同步的(模型的规则),我们可以直接调用它们
代码段 1.3:通知器
private void notifyListenersOfEndOfStream() {
for (NumberReadListener numberReadListener : listeners) {
numberReadListener.numberStreamTerminated(new NumberReadEvent(this, 0D));
}
}
private void notifyListeners(Double d) {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(new NumberReadEvent(this, d));
}
}
|
请注意,我们在这里做了一些假设。首先,我们假设我们将从某个地方获取 Double 值 d。此外,我们假设没有监听器会关心流结束通知中的数字值,并且为此事件传递了固定值 0。
最后,我们必须处理读取数字流。我们将使用 Console 类来完成此操作,并且只需不断读取数字,直到没有更多数字为止
代码段 1.4:main 方法
public void start() {
Console console = System.console();
if (console != null) {
Double d = null;
do {
String readLine = console.readLine("Enter a number: ", (Object[])null);
d = getDoubleValue(readLine);
if (d != null) {
notifyListeners(d);
}
} while (d != null);
notifyListenersOfEndOfStream();
}
}
|
请注意,我们如何通过调用 notify 方法将数字读取循环挂钩到事件处理机制?整个类看起来像这样
代码清单 1.4:NumberReader。
package org.wikibooks.en.javaprogramming.example;
import java.io.Console;
import java.util.HashSet;
import java.util.Set;
public class NumberReader {
private Set<NumberReadListener> listeners;
public NumberReader() {
listeners = new HashSet<NumberReadListener>();
}
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
public void start() {
Console console = System.console();
if (console != null) {
Double d = null;
do {
String readLine = console.readLine("Enter a number: ", (Object[])null);
d = getDoubleValue(readLine);
if (d != null) {
notifyListeners(d);
}
} while (d != null);
notifyListenersOfEndOfStream();
}
}
private void notifyListenersOfEndOfStream() {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberStreamTerminated(new NumberReadEvent(this, 0D));
}
}
private void notifyListeners(Double d) {
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(new NumberReadEvent(this, d));
}
}
private Double getDoubleValue(String readLine) {
Double result;
try {
result = Double.valueOf(readLine);
} catch (Exception e) {
result = null;
}
return result;
}
}
|
运行示例
[edit | edit source]最后,我们还需要一个类:应用程序的启动点。此类将包含一个 main() 方法,以及代码来创建 NumberReader、一个监听器以及将两者组合起来
代码清单 1.5:Main。
package org.wikibooks.en.javaprogramming.example;
public class Main {
public static void main(String[] args) {
NumberReader reader = new NumberReader();
NumberReadListener listener = new NumberReadListenerImpl();
reader.addNumberReadListener(listener);
reader.start();
}
}
|
如果您编译并运行该程序,结果将类似于以下内容
示例运行 >java org.wikibooks.en.javaprogramming.example.Main 输入一个数字: 0.1 输入一个数字: 0.2 输入一个数字: 0.3 输入一个数字: 0.4 输入一个数字: |
输出 数字流的总和: 1.0 |
接下来,让我们看看如何将适配器应用于我们的设计。适配器用于为事件处理过程添加功能,这些功能
- 对于该过程来说是通用的,而不是特定于某个监听器;或者
- 不应该影响特定监听器的实现。
根据事件模型规范,适配器的典型用例是为事件添加路由逻辑。但你也可以添加过滤或日志记录。在我们的示例中,让我们这样做:将数字作为计算的“证据”记录到日志中。
如前所述,适配器是一个位于事件源和监听器之间的类。从事件源的角度来看,它伪装成一个监听器(因此它必须实现监听器接口)。从监听器的角度来看,它假装是事件源(因此它应该具有添加和删除方法)。换句话说,要编写适配器,你必须从事件源重复一些代码(以管理监听器),并且你必须重新实现事件通知方法以执行一些额外的操作,然后将事件传递给实际的监听器。
在我们的示例中,我们需要一个将数字写入日志文件的适配器。为了保持简单,让我们创建一个适配器,它
- 使用固定的日志文件名并用每次程序运行覆盖该日志文件。
- 在构造函数中打开一个
FileWriter
并始终保持打开状态。 - 通过将数字写入
FileWriter
来实现numberRead
方法。 - 通过关闭
FileWriter
来实现numberStreamTerminated
方法。
此外,我们可以通过从 NumberReader
类中复制所有我们需要管理监听器的代码来简化操作。同样,在实际程序中,你可能想要以不同的方式执行此操作。请注意,每个通知方法的实现也会将事件传递给所有真正的监听器
代码清单 1.6:NumberReaderLoggingAdaptor。
package org.wikibooks.en.javaprogramming.example;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class NumberReaderLoggingAdaptor implements NumberReadListener {
private Set<NumberReadListener> listeners;
private BufferedWriter output;
public NumberReaderLoggingAdaptor() {
listeners = new HashSet<NumberReadListener>();
try {
output = new BufferedWriter(new FileWriter("numberLog.log"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void addNumberReadListener(NumberReadListener listener) {
this.listeners.add(listener);
}
public void removeNumberReadListener(NumberReadListener listener) {
this.listeners.remove(listener);
}
@Override
public void numberRead(NumberReadEvent numberReadEvent) {
try {
output.write(numberReadEvent.getNumber() + "\n");
} catch (Exception e) {
}
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberRead(numberReadEvent);
}
}
@Override
public void numberStreamTerminated(NumberReadEvent numberReadEvent) {
try {
output.flush();
output.close();
} catch (Exception e) {
}
for (NumberReadListener numberReadListener: listeners) {
numberReadListener.numberStreamTerminated(numberReadEvent);
}
}
}
|
当然,要使适配器正常工作,我们必须对引导代码进行一些更改
代码清单 1.7:Main。
package org.wikibooks.en.javaprogramming.example;
public class Main {
public static void main(String[] args) {
NumberReader reader = new NumberReader();
NumberReadListener listener = new NumberReadListenerImpl();
NumberReaderLoggingAdaptor adaptor = new NumberReaderLoggingAdaptor();
adaptor.addNumberReadListener(listener);
reader.addNumberReadListener(adaptor);
reader.start();
}
}
|
但请注意,我们可以在系统中轻松重新链接对象。适配器和监听器都实现了监听器接口,并且适配器和事件源看起来都像事件源,这意味着我们可以将适配器连接到系统,而无需更改我们之前开发的类中的任何语句。
当然,如果我们运行与上述示例相同的示例,数字现在将记录在日志文件中。
如前所述,事件模型在 Java 平台中没有一个单一的、包罗万象的实现。相反,该模型是几个不同用途的实现的基础,这些实现既存在于标准 Java 平台中,也存在于平台之外(在框架中)。
在平台内,主要的实现存在于两个领域