Java - 事件處理機制

Java - 事件處理機制


一、觀察者模式

瞭解事件和監聽,需要先了解觀察者模式。

接下來介紹一個觀察者模式的場景:

  1. 老師佈置作業,通知學生;
  2. 學生觀察到老師佈置了作業,開始做作業

在這個場景中,學生就是觀察者,老師是被觀察者。但是:

教師作爲被觀察者,實際上把握主動。

接下來實現上面的場景:

v2-8d6726829e25797ba881cef61ebf84d7_hd.jpg-36.9kB

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 觀察者模式用意

  1. 在代碼中我們可以發現教師類和學生類無關,並只依賴java.util.Observable。如果講課範圍擴大,比如也需要給其他老師講課,那麼也只需要老師實現java.util.Observer,並且將其他老師加入授課老師的觀察者列表中即可。
  2. 觀察者分離了觀察者和被觀察者自身的責任,讓類各自維護自己的功能,提高了系統的可重用性;
  3. 觀察看上去是一個主動的行爲,但是其實觀察者不是主動調用自己的業務代碼的,相反,是被觀察者調用的。所以,觀察者模式還有另一個名字,叫發佈-訂閱模式。

觀察者模式還有另外一種形態,就是事件驅動模型,這兩種方式在實現機制上是非常接近的,在理解了觀察者模式的基礎上,理解事件驅動,就非常簡單了。

二、事件驅動模型初窺

事件驅動模型是觀察者模式的升級,其中的對應關係爲:

  1. 觀察者對應監聽器(學生)
  2. 被觀察者對應事件源(教師)

在這裏:事件源產生事件,事件帶有事件源,監聽器則監聽事件。其中一共會牽扯四個類

  1. 事件源(即教師,被觀察者)
  2. 事件
  3. 監聽器接口
  4. 具體的監聽器(即學生,觀察者)

而在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 老師佈置作業的事件驅動模型版本

類圖如下:

v2-5bcecc4f7be03812ae70eb947988106b_hd.jpg-39.9kB

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);
    }

}
  1. Teacher沒有父類了,Teacher作爲事件中的事件源Source被封裝到HomeworkEventObject中了。這沒有什麼不好的,業務對象和框架代碼隔離開來,解耦的非常好,但是正因爲如此,我們需要在Teacher中自己維護一個Student的列表,於是,我們看到了homeworkListenerList這個變量
  2. 在觀察者模式中,我們直接調用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 總結

從客戶端的角度來說,我們幾乎完全沒有更改任何地方,跟觀察者模式的客戶端代碼一模一樣,但是內部的實現機制上,我們卻使用了事件機制。

現在我們來總結下,觀察者模式和事件驅動模型的幾個不同點:

  1. 事件源不再繼承任何模式或者模型本身的父類,徹底將業務代碼解耦出來;
  2. 在事件模型中,每個監聽者(觀察者)都需要實現一個自己的接口。沒錯,比如鼠標事件,分別有單擊、雙擊、移動等等的事件,這分別就是增加了代碼的靈活性;

三、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);
    }
}

至此,我們可以畫出清晰的類圖了:

v2-4f347701be1d8872b31b92cd22cf15d8_hd.jpg-48.7kB

接下來實現客戶端代碼:

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) {
        }

    }
}

接下來把監聽器、事件處理器、事件、事件源都指出來。

  1. 監聽器:DemoFrame就是監聽器,對應ConcreteMouseListener;
  2. 事件處理器:MouseClicked方法就是監聽器,ConcreteMouseListener裏面也有這個方法;
  3. 事件:JAVA運行時捕獲到硬件鼠標觸發,從而調用了事件處理器,在事件處理器內部生成的MouseEvent,就是事件;
  4. 事件源: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]


參考文章:

  1. https://zhuanlan.zhihu.com/p/27185877
  2. https://zhuanlan.zhihu.com/p/27273286
  3. https://zhuanlan.zhihu.com/p/27294617
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章