public interface MouseListener {
void mouseClicked(MouseEvent e);
void mouseEntered(MouseEvent e);
void mouseExited(MouseEvent e);
void mousePressed(MouseEvent e);
void mouseReleased(MouseEvent e);
}
public interface MouseMotionListener {
void mouseDragged(MouseEvent e);
void mouseMoved(MouseEvent e);
}
public class MyPanel extends Panel implements MouseListener {
private Canvas canvas = ...;
public MyPanel() {
...
canvas.addMouseListener(this);
}
public void mouseEntered(MouseEvent e) { ... }
public void mouseExited(MouseEvent e) { ... }
public void mousePressed(MouseEvent e) { ... }
public void mouseReleased(MouseEvent e) { ... }
}
由于注册事件时仅用到一个字段,而interface中不同事件的分派由类(如例中的MyPanel)的vtbl支持,被类的所有对象共享;因而当类有多个实例时,Java的事件注册机制较C#为每个事件保存单独的delegate字段更为节省。如果上面的代码用C#实现,那么所有MyPanel的实例都重复相同的mouseEntered、mouseExited等字段。
应当补充指出,在我的印象中早期JDK(1.1?)AWT控件似乎用1个AWTEventMulticaster字段来注册所有不同类型的EventListener,因而较节省。我在网上看到的一些API文档(包括AWTEventMulticaster的实现)与我的印象不符。我认为一方面私人或公司有权建立自己的知识库(如光盘资料),国家依赖于它们的专业知识而不是反过来;另一方面,篡改API等资料是不可容忍的自贬人格的行为,会导致中国人受歧视。
当需要注册多个EventListener,尤其需要为不同对象注册相同类型的EventListener时,匿名EventListener类型常被使用。
public class MyPanel extends Panel implements MouseListener {
private Canvas canvas1 = ...;
private Canvas canvas2 = ...;
public MyPanel() {
...
canvas1.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) { ... }
public void mousePressed(MouseEvent e) { ... }
...
});
canvas2.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) { ... }
public void mousePressed(MouseEvent e) { ... }
...
});
}
}
不过,匿名EventListener类型导致更多的class loading与对象实例,可能成为系统运行的负担。(Java虚拟机可以做的是,由于EventListener对象与所属控件对象(MyPanel)一一对应的关系,不另外为EventListener分配对象空间,而将其嵌入所属控件(MyPanel)的对象空间内,并且优化掉OuterClass.this字段。)
不难想像如下的语言扩展能够简化语法,并帮助Java虚拟机更高效地执行。public class MyPanel extends Panel
implements MouseListener-canvas1, MouseListener-canvas2 {
private Canvas canvas1 = ...;
private Canvas canvas2 = ...;
public MyPanel() {
...
canvas1.addMouseListener((-canvas1)this);
canvas2.addMouseListener((-canvas2)this);
}
private void mouseEntered-canvas1(MouseEvent e) { ... }
private void mousePressed-canvas1(MouseEvent e) { ... }
...
private void mouseEntered-canvas2(MouseEvent e) { ... }
private void mousePressed-canvas2(MouseEvent e) { ... }
...
}
例中,MyPanel实现了两个MouseListener接口,用后缀予以区别;不同接口的实现方法不同。例中的代码与下述实现等效;注意,因为interface的实现方法是private,所以两个interface也被声明为private,因而MyPanel的接口声明对外不可见。
public class MyPanel extends Panel
implements MyPanel.MouseListener$canvas1, MyPanel.MouseListener$canvas2 {
private interface MouseListener$canvas1 extends MouseListener {}
private interface MouseListener$canvas2 extends MouseListener {}
private Canvas canvas1 = ...;
private Canvas canvas2 = ...;
public MyPanel() {
...
canvas1.addMouseListener((MouseListener$canvas1)this);
canvas2.addMouseListener((MouseListener$canvas2)this);
}
private void MouseListener$canvas1.mouseEntered(MouseEvent e) { ... }
private void MouseListener$canvas1.mousePressed(MouseEvent e) { ... }
...
private void MouseListener$canvas2.mouseEntered(MouseEvent e) { ... }
private void MouseListener$canvas2.mousePressed(MouseEvent e) { ... }
...
}
在同一类中多次实现相同接口,在JUnit的测试代码中也很有用。在下面的例子中,test1与test2有相同的setup()与tearDown(),但有各自的run()方法。注意,例子仅用到Java语言本身而未依赖注解及反射,因而代码执行效率更高。
public class MyTest implements TestCase-test1, TestCase-test2 {
public void setup() { ... }
public void tearDown() { ... }
public void run-test1() { ... }
public void run-test2() { ... }
}
public class Runner {
public void run(TestCase testCase) { ... }
public void run(Object test) {
for (TestCase t : TestCase.class.casts(test))
run(t);
}
}
另一个可能的语言改进是动态分派(dynamic dispatch,有时也称double dispatch),即根据参数的运行时类型动态在多个方法之中选择最匹配的一个。
public interface VirtualEventListener {
default void processEvent(virtual AWTEvent evt) {}
}
public MyPanel extends Panel implements VirtualEventListener {
private Canvas canvas = ...;
public MyPanel() {
...
canvas.addVirtualEventListener(this);
}
public void processEvent(MouseEvent evt) { ... }
public void processEvent(KeyEvent evt) { ... }
}
说明:
1. 接口声明中的virtual表示需要动态匹配的参数,一个方法可以同时包含多个需要动态匹配的参数(double dispatch),也可以同时有动态与非动态匹配的参数。2. 运行时的方法匹配与静态的方法overload相似,只是根据运行时的类型。Java虚拟机将各方法按参数类型的继承关系组织成树结构,并找出最深的匹配节点。(继承关系可能能用位码比较完成。有多个动态参数及需要匹配interface时更复杂。)
3. 按上述方法实现的动态分派开销比虚方法调用高,但能减少EventListener的字段数量(仅需1个字段)及vtbl的大小(仅处理用到的事件类型),并使代码清晰可读。
4. 事件按照源(source)的不同分派给不同的方法,也可能被合并到动态分派之中?
此外,double dispatch也可以通过两次虚函数调用实现,手写代码也不困难。
public class AWTEvent {
...
public void processBy(Component component) {
component.processEvent(this);
}
}
public class MouseEvent extends AWTEvent {
...
public void processBy(Component component) {
component.processMouseEvent(this);
}
}
public class KeyEvent extends AWTEvent {
...
public void processBy(Component component) {
component.processKeyEvent(this);
}
}
public class Component {
...
public void processEvent(AWTEvent evt) { evt.processBy(this); }
protected void processEvent (Event evt) { } // AWT中已有这些方法,
protected void processMouseEvent(MouseEvent evt) { } // 但未用double dispatch
protected void processKeyEvent (KeyEvent evt) { }
...
}
public class MyComponent : Component {
...
protected void processMouseEvent(MouseEvent evt) { }
protected void processKeyEvent (KeyEvent evt) { }
}
用虚方法实现的double dispatch效率要高得多。不过,这要求为新添的Event类型修改Component基类。如果Java编译工具或虚拟机能够代为生成double dispatch代码则要方便得多。另外需要指出的是,从Oracle获取的JDK AWT源码实际上相当混乱而不可读。