关于Java的事件分派机制及两个可能的语言扩展

来源:互联网 发布:智慧芽数据库 编辑:程序博客网 时间:2024/06/18 09:27
Java用interface将Event分组,并按组同时注册interface中的所有事件。

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源码实际上相当混乱而不可读。


0 0
原创粉丝点击