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