【java】Observer和Observable详解

转载请标明出处:http://blog.csdn.net/u012250875/article/details/77747878

1.必要性

1.1 观察者模式是oo设计中经常用到的模式之一,大家在解决实际需求时,观察者模式往往都会用到,而javase中已经提供了Observer接口和Observable类让你简单快速的实现观察者模式,因此有必要去了解Observer和Observable;

2.观察者模式概述

2.1 角色:被观察对象,观察者

2.2 关系:
1).被观察对象:观察者 = 1:n
2).被观察对象状态发生变化,会通知所有观察者,观察者将做出相应的反应

2.3 详细说明:参见【设计模式】观察者模式

3.源码分析

3.1 Observer接口
Observer为java.util包下的一个接口,源码如下:

public interface Observer {
    void update(Observable o, Object arg);
}

该接口约定了观察者的行为。所有观察者需要在被观察对象发生变化时做出相应的反应,所做的具体反应就是实现Observer接口的update方法,实现update方法时你可以用到两个参数,一个参数的类型是Observable,另一个参数的类型是Object。当然如果完全由自己去实现一个观察者模式的方案,自己去设计Observer接口时,可能不会设计这两个参数。那为什么jdk设计该接口时规定接口中有这两个参数呢?那就是通用性。想想整个观察者模式有哪些类之间需要交互?使用该模式时牵扯三个类,一个是观察者,一个是被观察对象,一个是调用者(调用者可以是被观察对象本身调用,更多情况是一个具体的业务类),当前接口代表观察者,要与被观察对象交互,因此update方法需要持有被观察对象(Observable)的引用,第一参数产生了;如何与调用者通信,则是添加了类型为Object的参数(该参数是调用者调用Observable实例的notifyObservers(Object obj)方法时传入的,当然也可以不传);第一个参数可以说是为观察者提供了一种拉取数据的方式,update中的业务可以根据所需去拉去自己想要的被观察对象的信息(一般被观察对象中提供getter),第二个参数则是由调用者调用notifyObservers(Object obj)将一些信息推过来。通过这两个参数,观察者,被观察对象,调用者(调用通知刷新方法的可能是被观察对象本身,此时只存在观察者与被观察者两者)三者就联系起来了。

3.2 Observable类
Observable同样是java.util包下的接口(理所当然的事),该类的成员变量和方法如下:

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;
    public Observable(){};
    protected synchronized void setChanged(){};
    protected synchronized void clearChanged(){};
    public synchronized void addObserver(Observer o){};
    public synchronized void deleteObserver(Observer o) {};
    public synchronized void deleteObservers(){};
    public synchronized boolean hasChanged(){};
    public synchronized int countObservers(){};
    public void notifyObservers(){};
    public void notifyObservers(Object arg){};
}

先说成员变量:
1)该类中含有一个boolean型的变量changed,代表是否发生改变了,Observable类只提供这个boolean值来表明是否发生变化,而不定义什么叫变化,因为每个业务中对变化的具体定义不一样,因此子类自己来判断是否变化;该变量既提供了一种抽象(变与不变),同时提供了一种观察者更新状态的可延迟加载,通过后面的notifyObservers方法分析可知观察者是否会调用update方法,依赖于changed变量,因此即使被观察者在逻辑上发生改变了,只要不调用setChanged,update是不会被调用的。如果我们在某些业务场景不需要频繁触发update,则可以适时调用setChanged方法来延迟刷新。

2)该类含有一个集合类Vector,该类泛型为Observer,主要用来存放所有观察自己的对象的引用,以便在更新时可以挨个遍历集合中的观察者,逐个调用update方法
说明:
1.8的jdk源码为Vector,有版本的源码是ArrayList的集合实现;
Vector这个类和ArrayList的继承体系是一致,主要有两点不同,一是Vector是线程安全的,ArrayList不是线程安全的,Vector的操作依靠在方法上加了同步关键字来保证线程安全,与此同时ArrayList的性能是要好于Vector的;二是Vector和ArrayList扩容阀值不太一样,ArrayList较Vector更节省空间;

再来说说方法:
1)操作changed变量的方法为setChanged(),clearChanged(),hasChanged();见名知意,第一个设置变化状态,第二清除变化状态,这两个的访问权限都是protected,表明这两个方法由子类去调用,由子类来告诉什么时候被观察者发生变化了,什么时候变化消失,而hasChanged()方法的访问权限是公有的,调用者可以使用该方法。三个方法都有同步关键字保证变量的读写操作线程安全。

2)操作Vector类型变量obs的方法为addObserver(Observer o), deleteObserver(Observer o), deleteObservers(),countObservers(),这四个方法分别实现了动态添加观察者,删除观察者,删除所有观察者,获取观察者数量。四个方法的访问权限都是公有的,这是提供给调用者的方法,让调用者来实时动态的控制哪些观察者来观察该被观察对象。

3)操作Vector型变量obs的四个方法都加有同步关键字,但是我们刚才分析成员属性Vector obs这个变量时,说Vector类型为线程安全的,而上述四个方法为什么还要加同步关键字呢,这是怎么回事?据我推测应该是程序员重构遗留问题吧,因为前面我说道,有历史版本的源码是使用的ArrayList来持有Observer的引用,而ArrayList不是线程安全的,所以上述四个操作结合的方法需要加上同步关键字来保证线程安全,而后来换成线程安全的Vector了,但这四个操作集合的方法依旧保留了同步关键字。

4)两个对外的方法notifyObservers(),notifyObservers(Object arg),该方法由调用者来操作,用来通知所有的观察者需要做更新操作了。

先不看源码,想想应该怎么做呢?通知观察者们进行刷新操作,不就是用for循环一个一个操作集合中的Observer调用update方法嘛,这还不简单,于是版本一产生:

//版本一
public void notifyObservers(Object arg) {
    if(changed){
        for (int i = 0; i<obs.size(); i++)
            obs.get(i).update(this, arg);
    }
}

看看版本一,很容易发现,该方法会出很多问题,首先调用了所有的观察者的update方法,但是没有清除被观察的变化状态,由于changed变量状态没有重置,因此,如果notifyObservers被多次调用,即使Observable没有再发生变化,所有观察者的update方法已经会被执行。因此需要进行修改,如下:

//版本二
public void notifyObservers(Object arg) {
    clearChanged();
    if(changed){
        for (int i = 0; i<obs.size(); i++)
            obs.get(i).update(this, arg);
    }
}

看看版本二,依旧有问题,如果出现并发时,各线程对changed变量的读写操作不安全,可能出现脏读因此产生重复update或者不能update的情况,因此需要进行修改,如下:

//版本三
public synchronized void notifyObservers(Object arg) {
    clearChanged();
    for (int i = 0; i<obs.size(); i++)
        obs.get(i).update(this, arg);
}

看看版本三,不会出现并发造成的变量状态不一致带来的错误操作,但是想一想观察者数量较多时或者update方法执行时间较长时,被观察者变化后,notifyObservers的执行时间大大增加,呈线性增长,比如并发数是20,而此时有10个线程发生changed并且调用了notifyObservers方法,那么10个线程执行该方法将进入同步,粗略计算耗时为10*for循环执行时长,因此需要进行修改,我们只对changed变量的读写部分加锁,不会引起变量状态的不一致性,同时当同步块的代码执行完毕后,该线程可以先去执行耗时的for循环,修改如下:

//版本四
public void notifyObservers(Object arg) {
    synchronized (this) {
        if (!changed)
           return;
        clearChanged();
    }
    for (int i = 0; i<obs.size(); i++)
        obs.get(i).update(this, arg);
}

看看版本四,发现还是存在问题虽然解决了耗,但依旧会有问题,多线程在同步块进行了同步,但是执行for循环的时候,由于调用者可能不断在增删观察者,假如A线程刚执行完i

//源码的方案
public void notifyObservers(Object arg) {
        Object[] arrLocal;
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

源码果然很严谨!!!

5)上面说了系统对观察者模式的支持有这么多优点,但依旧不可避免以下几个缺点:
A. Observable是一个具体实现类,面向细节了,而未面向抽象
B. 使用Observable时需要使用继承,由于java的类单继承性,如果你的类已经继承了一个类,将不能继承Observable来实现观察者模式,并且由于setChanged和clearChanged方法都是protected的,所以你也不能通过组合来完成观察者模式

4.应用

观察者模式是很常用的模式,尤其在界面编程,比如android中的BaseAdapter,就使用了观察者模式,当数据源发生变化时,通知界面重新绘制。下面我们来利用jdk中提供的Observer和Observable来实现一个观察者模式的例子。

需求:
镇上来了一位小丑,为大家表演节目,所有观看的观众会根据小丑表演的精彩与否来做出相应的反应,比如表演的好就鼓掌喝彩,表演的不好就倒喝彩,表演完毕观众就退场。

分析:
这里涉及到小丑和观众,小丑的数量是1,而观众的数量是n,观众会对小丑的举动做出相应的反应。这里很符合观察者模式的场景,因此,我们需要创建一个小丑类Clown,作为被观察对象,因此需要继承Observable类,同时具有表演的行为,退场的行为;同时需要创建一个观众类Viewer,作为观察者,因此需要实现Observer接口,同时观众具有喝彩的行为,倒喝彩的行为,退场的行为,每个观众还对应一个座位号。

实现:

import java.util.Observable;
import java.util.Random;

/** * @author puyafeng * @desc 小丑类 */
public class Clown extends Observable {
    /** 表演的精彩 */
    public static final int PERFORM_GOOD = 0;
    /** 表演的糟糕 */
    public static final int PERFORM_BAD = 1;
    /** 表演完毕 */
    public static final int PERFORM_COMPLETE = 2;

    /** * 表演 */
    public void perform() {
        System.out.println("**小丑开始表演**");

        int random = new Random().nextInt(2);
        //小丑表演状态是随机值,0表演的好,1表演的差
        switch (random) {
            case PERFORM_GOOD:
                System.out.println("**小丑状态很好,表演的很精彩!**");
                break;
            case PERFORM_BAD:
                System.out.println("**小丑状态不好,出了点篓子!**");
                break;
        }
        setChanged();
        notifyObservers(random);//表演好坏通过该参数传递到观众的update方法的第二个参数上
    }

    /** * 表演结束,小丑退场 */
    public void exit() {
        System.out.println("**表演结束,小丑退场!**");
        setChanged();
        notifyObservers(PERFORM_COMPLETE);//退场消息通过该参数传递到观众的update方法的第二个参数上
    }

}
import java.util.Observable;
import java.util.Observer;

/** * @author puyf * @desc 观众类 */
public class Viewer implements Observer {
    private int seatNo;

    public Viewer(int seatNo) {
        this.seatNo = seatNo;
    }

    @Override
    public void update(Observable o, Object arg) {
        Integer state = (Integer) arg;
        switch (state) {
            case Clown.PERFORM_GOOD:
                applause();
                break;
            case Clown.PERFORM_BAD:
                CheerBack();
                break;
            case Clown.PERFORM_COMPLETE:
                exit();
                break;
            default:
                break;
        }
    }

    /** * 鼓掌 */
    private void applause() {
        System.out.println("座位号" + getSeatNo() + "的观众鼓掌了!");
    }

    /** * 倒喝彩 */
    private void CheerBack() {
        System.out.println("座位号" + getSeatNo() + "的观众发出了倒喝彩!");
    }

    /** * 退场 */
    private void exit() {
        System.out.println("座位号" + getSeatNo() + "的观众退场!");
    }

    public int getSeatNo() {
        return seatNo;
    }

}
/** * * @author puyf * @desc 测试类 */
public class Test {
    public static void main(String[] args) {
        //来了一个小丑
        Clown clown = new Clown();
        //观众入场了
        for (int i = 0; i < 20; i++) {
            Viewer v = new Viewer(i);
            clown.addObserver(v);
            System.out.println("座号为"+v.getSeatNo()+"的观众入座");
        }
        //小丑开始表演
        clown.perform();
        //小丑表演完毕,退场
        clown.exit();
    }
}

执行结果:

座号为0的观众入座
座号为1的观众入座
座号为2的观众入座
座号为3的观众入座
座号为4的观众入座
座号为5的观众入座
座号为6的观众入座
座号为7的观众入座
座号为8的观众入座
座号为9的观众入座
座号为10的观众入座
座号为11的观众入座
座号为12的观众入座
座号为13的观众入座
座号为14的观众入座
座号为15的观众入座
座号为16的观众入座
座号为17的观众入座
座号为18的观众入座
座号为19的观众入座
**小丑开始表演**
**小丑状态不好,出了点篓子!**
座位号19的观众发出了倒喝彩!
座位号18的观众发出了倒喝彩!
座位号17的观众发出了倒喝彩!
座位号16的观众发出了倒喝彩!
座位号15的观众发出了倒喝彩!
座位号14的观众发出了倒喝彩!
座位号13的观众发出了倒喝彩!
座位号12的观众发出了倒喝彩!
座位号11的观众发出了倒喝彩!
座位号10的观众发出了倒喝彩!
座位号9的观众发出了倒喝彩!
座位号8的观众发出了倒喝彩!
座位号7的观众发出了倒喝彩!
座位号6的观众发出了倒喝彩!
座位号5的观众发出了倒喝彩!
座位号4的观众发出了倒喝彩!
座位号3的观众发出了倒喝彩!
座位号2的观众发出了倒喝彩!
座位号1的观众发出了倒喝彩!
座位号0的观众发出了倒喝彩!
**表演结束,谢谢各位观看,请各位观众退场!**
座位号19的观众退场!
座位号18的观众退场!
座位号17的观众退场!
座位号16的观众退场!
座位号15的观众退场!
座位号14的观众退场!
座位号13的观众退场!
座位号12的观众退场!
座位号11的观众退场!
座位号10的观众退场!
座位号9的观众退场!
座位号8的观众退场!
座位号7的观众退场!
座位号6的观众退场!
座位号5的观众退场!
座位号4的观众退场!
座位号3的观众退场!
座位号2的观众退场!
座位号1的观众退场!
座位号0的观众退场!

当然上面的Clown类中的perform方法和exit方法中调用了notifyObservers(Object obj)方法,有时候是在业务逻辑中调用该方法来通知,比如去掉perform和exit中的notifyObservers,而在我们这里的Test类中的main()方法 来根据具体的业务逻辑来调用notifyObservers并传入参数。

    原文作者:puyf
    原文地址: https://blog.csdn.net/u012250875/article/details/77747878
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞