美文网首页
Java - 事件处理机制

Java - 事件处理机制

作者: 寒沧 | 来源:发表于2018-04-11 22:14 被阅读38次

    Java - 事件处理机制


    一、观察者模式

    了解事件和监听,需要先了解观察者模式。

    接下来介绍一个观察者模式的场景:

    1. 老师布置作业,通知学生;
    2. 学生观察到老师布置了作业,开始做作业

    在这个场景中,学生就是观察者,老师是被观察者。但是:

    教师作为被观察者,实际上把握主动。

    接下来实现上面的场景:

    <center> v2-8d6726829e25797ba881cef61ebf84d7_hd.jpg-36.9kBv2-8d6726829e25797ba881cef61ebf84d7_hd.jpg-36.9kB

    </center>

    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 老师布置作业的事件驱动模型版本

    类图如下:

    <center> v2-5bcecc4f7be03812ae70eb947988106b_hd.jpg-39.9kBv2-5bcecc4f7be03812ae70eb947988106b_hd.jpg-39.9kB

    </center>

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

    至此,我们可以画出清晰的类图了:

    <center> v2-4f347701be1d8872b31b92cd22cf15d8_hd.jpg-48.7kBv2-4f347701be1d8872b31b92cd22cf15d8_hd.jpg-48.7kB

    </center>

    接下来实现客户端代码:

    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

    相关文章

      网友评论

          本文标题:Java - 事件处理机制

          本文链接:https://www.haomeiwen.com/subject/iqhtkftx.html