關於Java的事件分派機制及兩個可能的語言擴展

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源碼實際上相當混亂而不可讀。


發佈了28 篇原創文章 · 獲贊 0 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章