Java - 事件處理機制
一、觀察者模式
瞭解事件和監聽,需要先了解觀察者模式。
接下來介紹一個觀察者模式的場景:
- 老師佈置作業,通知學生;
- 學生觀察到老師佈置了作業,開始做作業
在這個場景中,學生就是觀察者,老師是被觀察者。但是:
教師作爲被觀察者,實際上把握主動。
接下來實現上面的場景:
1.1 觀察者
場景中的觀察者是:學生
package event;
import java.util.Observable;
/**
* Created by Joe on 2018/4/11
*/
public class Student implements java.util.Observer {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
Teacher teacher = (Teacher) o;
System.out.printf("學生%s觀察到(實際是被通知)%s佈置了作業《%s》 \n", this.name, teacher.getName(), arg);
}
}
1.2 被觀察者
在這個場景中是:老師
package event;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Joe on 2018/4/11
*/
public class Teacher extends java.util.Observable {
private String name;
private List<String> books;
public Teacher(String name) {
this.name = name;
this.books = new ArrayList<>();
}
public String getName() {
return name;
}
public void setHomework(String homework) {
System.out.println(this.name + "佈置的作業爲:" + homework);
books.add(homework);
setChanged();
notifyObservers(homework);
}
}
1.3 測試
package event;
/**
* Created by Joe on 2018/4/11
*/
public class Clients {
public static void main(String[] args) {
Student student1= new Student("張三");
Student student2 = new Student("李四");
Teacher teacher1 = new Teacher("菜");
teacher1.addObserver(student1);
teacher1.addObserver(student2);
teacher1.setHomework("事件機制第一天作業");
}
}
代碼的運行結果爲:
菜佈置的作業爲:事件機制第一天作業
學生李四觀察到(實際是被通知)菜佈置了作業《事件機制第一天作業》
學生張三觀察到(實際是被通知)菜佈置了作業《事件機制第一天作業》
在語義理解上面,觀察是一個主動行爲,但是在代碼實現中,update()
方法是由”被觀察者”Teacher
主動調用,具體的調用代碼如下:
setChanged();
notifyObservers(homework);
更具體的部分我們可以藉助IDE進入方法體的源碼中進行查看,主動調用觀察者進行操作的是notifyObservers()
方法,該方法的參數如下:
/**
* If this object has changed, as indicated by the
* <code>hasChanged</code> method, then notify all of its observers
* and then call the <code>clearChanged</code> method to indicate
* that this object has no longer changed.
* <p>
* Each observer has its <code>update</code> method called with two
* arguments: this observable object and the <code>arg</code> argument.
*
* @param arg any object.
* @see java.util.Observable#clearChanged()
* @see java.util.Observable#hasChanged()
* @see java.util.Observer#update(java.util.Observable, java.lang.Object)
*/
public void notifyObservers(Object arg)
這個方法中調用update
方法的代碼如下:
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
通過for循環依次調用觀察者的update
方法。
1.4 觀察者模式用意
- 在代碼中我們可以發現教師類和學生類無關,並只依賴
java.util.Observable
。如果講課範圍擴大,比如也需要給其他老師講課,那麼也只需要老師實現java.util.Observer
,並且將其他老師加入授課老師的觀察者列表中即可。 - 觀察者分離了觀察者和被觀察者自身的責任,讓類各自維護自己的功能,提高了系統的可重用性;
- 觀察看上去是一個主動的行爲,但是其實觀察者不是主動調用自己的業務代碼的,相反,是被觀察者調用的。所以,觀察者模式還有另一個名字,叫發佈-訂閱模式。
觀察者模式還有另外一種形態,就是事件驅動模型,這兩種方式在實現機制上是非常接近的,在理解了觀察者模式的基礎上,理解事件驅動,就非常簡單了。
二、事件驅動模型初窺
事件驅動模型是觀察者模式的升級,其中的對應關係爲:
- 觀察者對應監聽器(學生)
- 被觀察者對應事件源(教師)
在這裏:事件源產生事件,事件帶有事件源,監聽器則監聽事件。其中一共會牽扯四個類
- 事件源(即教師,被觀察者)
- 事件
- 監聽器接口
- 具體的監聽器(即學生,觀察者)
而在JDK中,有現成的監聽器接口,代碼如下:
package java.util;
/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}
甚至連一個聲明的方法都沒有,那它存在的意義在哪?還記得面向對象中的上溯造型嗎,所以它的意義就在於告訴所有的調用者,我是一個監聽器。
上溯造型指將衍生類的句柄賦給基礎類的句柄(即是將子類的句柄賦給父類的句柄,也即把子類當做父類處理的過程),因爲它是從一個更特殊的類型到一個更常規的類型,所以上溯造型肯定是安全的。
接下來繼續看事件,事件裏面會含有getSource
方法,這個方法返回的是事件源(即教師,被觀察者)對象。
/*
* Copyright (c) 1996, 2003, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.util;
/**
* <p>
* The root class from which all event state objects shall be derived.
* <p>
* All Events are constructed with a reference to the object, the "source",
* that is logically deemed to be the object upon which the Event in question
* initially occurred upon.
*
* @since JDK1.1
*/
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
/**
* The object on which the Event initially occurred.
*/
protected transient Object source;
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @exception IllegalArgumentException if source is null.
*/
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
/**
* The object on which the Event initially occurred.
*
* @return The object on which the Event initially occurred.
*/
public Object getSource() {
return source;
}
/**
* Returns a String representation of this EventObject.
*
* @return A a String representation of this EventObject.
*/
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}
事件驅動模型中,JDK的設計者們進行了高級的抽象,就是讓上層類只是代表了:我是一個事件(含有事件源),或,我是一個監聽者!
2.1 老師佈置作業的事件驅動模型版本
類圖如下:
2.2 觀察者接口
觀察者接口(學生)。由於在事件驅動模型中,只有一個沒有任何方法的接口,EventListener,所以,我們可以先實現一個自己的接口。爲了跟上一篇的代碼保持一致,在該接口中我們聲明的方法的名字也叫update。注意,我們當然也可以不取這個名字,甚至還可以增加其它的方法聲明。
package events;
import java.util.Observable;
/**
* Created by Joe on 2018/4/11
*/
public interface HomeworkListener extends java.util.EventListener {
public void update(HomeworkEventObject o, Object arg);
}
2.3 觀察者類
學生
package events;
/**
* Created by Joe on 2018/4/11
*/
public class Student implements HomeworkListener{
private String name;
public Student(String name){
this.name = name;
}
@Override
public void update(HomeworkEventObject o, Object arg) {
Teacher teacher = o.getTeacher();
System.out.printf("學生%s觀察到(實際是被通知)%s佈置了作業《%s》 \n", this.name, teacher.getName(), arg);
}
}
2.4 事件子類
package events;
/**
* Created by Joe on 2018/4/11
*/
public class HomeworkEventObject extends java.util.EventObject {
public HomeworkEventObject(Object source) {
super(source);
}
public HomeworkEventObject(Teacher teacher) {
super(teacher);
}
public Teacher getTeacher(){
return (Teacher) super.getSource();
}
}
2.5 被觀察者
教師
package events;
/**
* Created by Joe on 2018/4/11
*/
import java.util.*;
public class Teacher {
private String name;
private List<String> homeworks;
/*
* 教師類要維護一個自己監聽器(學生)的列表,爲什麼?
* 在觀察者模式中,教師是被觀察者,繼承自java.util.Observable,Observable中含了這個列表
* 現在我們沒有這個列表了,所以要自己創建一個
*/
private Set<HomeworkListener> homeworkListenerList;
public String getName() {
return this.name;
}
public Teacher(String name) {
this.name = name;
this.homeworks = new ArrayList<String>();
this.homeworkListenerList = new HashSet<HomeworkListener>();
}
public void setHomework(String homework) {
System.out.printf("%s佈置了作業%s \n", this.name, homework);
homeworks.add(homework);
HomeworkEventObject event = new HomeworkEventObject(this);
/*
* 在觀察者模式中,我們直接調用Observable的notifyObservers來通知被觀察者
* 現在我們只能自己通知了~~
*/
for (HomeworkListener listener : homeworkListenerList) {
listener.update(event, homework);
}
}
public void addObserver(HomeworkListener homeworkListener){
homeworkListenerList.add(homeworkListener);
}
}
- Teacher沒有父類了,Teacher作爲事件中的事件源Source被封裝到HomeworkEventObject中了。這沒有什麼不好的,業務對象和框架代碼隔離開來,解耦的非常好,但是正因爲如此,我們需要在Teacher中自己維護一個Student的列表,於是,我們看到了homeworkListenerList這個變量
在觀察者模式中,我們直接調用Observable的notifyObservers來通知被觀察者,現在我們只能靠自己了,於是我們看到了這段代碼
for (HomeworkListener listener : homeworkListenerList) { listener.update(event, homework); }
2.6 客戶端代碼
package events;
/**
* Created by Joe on 2018/4/11
*/
public class Clients {
public static void main(String[] args) {
Student student1= new Student("張三");
Student student2 = new Student("李四");
Teacher teacher1 = new Teacher("zuikc");
teacher1.addObserver(student1);
teacher1.addObserver(student2);
teacher1.setHomework("事件機制第二天作業");
}
}
2.7 總結
從客戶端的角度來說,我們幾乎完全沒有更改任何地方,跟觀察者模式的客戶端代碼一模一樣,但是內部的實現機制上,我們卻使用了事件機制。
現在我們來總結下,觀察者模式和事件驅動模型的幾個不同點:
- 事件源不再繼承任何模式或者模型本身的父類,徹底將業務代碼解耦出來;
- 在事件模型中,每個監聽者(觀察者)都需要實現一個自己的接口。沒錯,比如鼠標事件,分別有單擊、雙擊、移動等等的事件,這分別就是增加了代碼的靈活性;
三、Java中的事件處理
3.1 鼠標點擊事件處理模型基礎版
對應HomeworkListener
,JDK中有MouseListener
,並且這個接口也繼承自EventListener
。
/**
* The listener interface for receiving "interesting" mouse events
* (press, release, click, enter, and exit) on a component.
* (To track mouse moves and mouse drags, use the
* <code>MouseMotionListener</code>.)
* <P>
* The class that is interested in processing a mouse event
* either implements this interface (and all the methods it
* contains) or extends the abstract <code>MouseAdapter</code> class
* (overriding only the methods of interest).
* <P>
* The listener object created from that class is then registered with a
* component using the component's <code>addMouseListener</code>
* method. A mouse event is generated when the mouse is pressed, released
* clicked (pressed and released). A mouse event is also generated when
* the mouse cursor enters or leaves a component. When a mouse event
* occurs, the relevant method in the listener object is invoked, and
* the <code>MouseEvent</code> is passed to it.
*
* @author Carl Quinn
*
* @see MouseAdapter
* @see MouseEvent
* @see <a href="https://docs.oracle.com/javase/tutorial/uiswing/events/mouselistener.html">Tutorial: Writing a Mouse Listener</a>
*
* @since 1.1
*/
public interface MouseListener extends EventListener
接下來我們對這個接口進行實現,命名爲ConcreteMouseListener
:
package events;
/**
* Created by Joe on 2018/4/11
*/
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class ConcreteMouseListener implements MouseListener {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("I haven been clicked by" + e.getSource().toString());
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
在這裏面,我們單獨爲單擊的事件處理器進行了代碼實現。
事件處理器:監聽器的具體實現類的實現方法,就叫事件處理器。
接下來需要注意的是MouseEvent
,首先看這個類的簡單用法:
/*
* 這裏的new Component() {} 就是 event.getSource() 得到的事件源 source
*/
MouseEvent event = new MouseEvent(new Component() {}, 1, 1, 1, 2, 3, 4,false);
在實際且正常的情況下,MouseEvent是沒有必要自己new的,JAVA運行時會捕獲硬件鼠標的點擊動作,由虛擬機底層爲我們生成該實例對象,這些構造函數參數中核心關鍵參數就是第一個參數new Component()
,回頭看看我們的教師學生版本是在哪裏生產事件的:
public void setHomework(String homework) {
System.out.printf("%s佈置了作業%s \n", this.name, homework);
homeworks.add(homework);
HomeworkEventObject event = new HomeworkEventObject(this);
/*
* 在觀察者模式中,我們直接調用Observable的notifyObservers來通知被觀察者
* 現在我們只能自己通知了~~
*/
for (HomeworkListener listener : homeworkListenerList) {
listener.update(event, homework);
}
}
是在Teacher的業務代碼setHomeworkf方法中。但是,在當前的我們要寫的這個例子中,new MouseEvent()
要在哪裏呢?我們在Button的業務代碼中進行調用。Button是誰,Button就類似Teacher,但又不完全等同Teacher,在Teacher中,Teacher本身就是事件源,所以它這個this作爲參數傳入進了HomeworkEventObject,而Button不能作爲參數傳入進MouseEvent,因爲我不打算讓Button繼承自Component,所以我們先new了一個臨時的Component。OK,分析到了這裏,我們自己的Button代碼大概就出來了,是這個樣子的:
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class Button {
private MouseListener mouseListener;
public void addMouseListener(MouseListener l) {
mouseListener = l;
}
public void doClick() {
/*
* 這裏的new Component() {} 就是 event.getSource() 得到的事件源 source
*/
MouseEvent event = new MouseEvent(new Component() {}, 1, 1, 1, 2, 3, 4, false);
//event.getSource();
this.mouseListener.mouseClicked(event);
}
}
至此,我們可以畫出清晰的類圖了:
接下來實現客戶端代碼:
public class Clients {
public static void main(String[] args) {
ConcreteMouseListener
listener = new ConcreteMouseListener();
Button button = new Button();
button.addMouseListener(listener);
button.doClick();
}
}
可以得到以下輸出:
I haven been clicked byevents.Button$1[,0,0,0x0,invalid]
3.2 正常窗體程序
接下來創建一個窗體,窗體上放置了一個按鈕,點擊了之後,執行了一行代碼。
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Created by Joe on 2018/4/11
*/
public class Clients {
public static void main(String[] args) {
new DemoFrame();
}
static class DemoFrame extends JFrame implements MouseListener {
public DemoFrame() {
super("demo");
this.setSize(500, 400);
this.setLocationRelativeTo(null);
this.getContentPane().setLayout(null);
this.setVisible(true);
JButton button1 = new JButton("ok");
button1.setBounds(8,
8, 80, 80);
button1.addMouseListener(this);
this.getContentPane().add(button1);
}
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("I haven been clicked by" + e.getSource().toString());
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
}
接下來把監聽器、事件處理器、事件、事件源都指出來。
- 監聽器:DemoFrame就是監聽器,對應ConcreteMouseListener;
- 事件處理器:MouseClicked方法就是監聽器,ConcreteMouseListener裏面也有這個方法;
- 事件:JAVA運行時捕獲到硬件鼠標觸發,從而調用了事件處理器,在事件處理器內部生成的MouseEvent,就是事件;
- 事件源:JAVA運行時捕獲到硬件鼠標觸發,從而調用了事件處理器,在事件處理器內部生成的target,就是事件源;
以上代碼的輸出爲:
I haven been clicked byjavax.swing.JButton[,8,8,80x80,alignmentX=0.0,alignmentY=0.5,border=javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@3ef244c,flags=296,maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14],paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,text=ok,defaultCapable=true]
參考文章: