设计模式之原型模式(十三)

原型模式是设计模式中算是简单的一种设计模式了,因为它有语言实现的支撑,只需要调用定义好了的方法即可使用,在写这篇设计模式之前,我看过很多资料,有几点比较疑惑的点,在这篇文章中验证,也顺便描述一下什么是原型模式。

 定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

这个定义也是很简单了,主要意思就是用一个实例当作是一个原型,通过拷贝这个实例去创建新的对象,就像西游记的美猴王使用猴毛去分身,可以很简单的创建各种美猴王对象,而不需要去初始化各种美猴王属性,比如身高体重之类。

先上一个简单的原型模式实例,再抛出问题。

/**
 * @description:
 * @author: linyh
 * @create: 2018-11-05 16:27
 **/
public class Weapon {

    private int size;

    public Weapon(int size) {
        this.size = size;
    }

    public int getSize() {
        return size;
    }

    public void changeSize(){
        size++;
    }
}
/**
 * @description:
 * @author: linyh
 * @create: 2018-11-05 16:27
 **/
public class Monkey implements Cloneable{

    private int age;
    private String name;
    private Weapon weapon;

    public Monkey() {
        this.age = 18;
        this.name = "猴子";
        this.weapon = new Weapon(10);
    }

    public void changeAge(){
        age ++;
    }

    public void changeName(){
        name = name + "changed ";
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public Weapon getWeapon() {
        return weapon;
    }
}

monkey中的change方法后面实验会用到。下面上测试类。

public class test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Monkey monkey = new Monkey();
        Monkey copyMonkey = (Monkey)monkey.clone();
        System.out.println("两个对象是否一样: " + (monkey == copyMonkey));
        System.out.println("正版猴子的各个属性: " + monkey.getName() +"," + monkey.getAge());
        System.out.println("复制猴子的各个属性: " + copyMonkey.getName() +"," + copyMonkey.getAge());
        System.out.println("两个猴子的武器是否一样: " + (monkey.getWeapon() == copyMonkey.getWeapon()));
    }
}

控制台打印

这里发现,克隆出来的对象是不一样的,如预期所料,属性都有初始化好,然后对象不同。但是这里的武器却还是同一个武器!这会引发什么问题呢?上测试代码

        System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
        System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
        monkey.getWeapon().changeSize();
        System.out.println("正版猴子武器SIZE改变成了" +monkey.getWeapon().getSize());
        System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());

控制台打印

可以看到,因为是同一个对象,我正版猴子把武器的size变化了,复制的猴子什么都没干武器也会变化,这确实不符合逻辑。

以上复制称为浅复制,意思是如果有引用类型,会把引用的地址直接复制过来,但如果是8种基本类型就没事,下面验证一下。

        System.out.println("正版的猴子的年龄: " + monkey.getAge());
        System.out.println("复制的猴子的年龄: " + copyMonkey.getAge());
        monkey.changeAge();
        System.out.println("正版猴子年龄改变成了" + monkey.getAge());
        System.out.println("复制的猴子的年龄: " + copyMonkey.getAge());

控制台打印

虽然我这里改变了正版猴子的年龄,但复制的猴子年龄依旧的18。

抛出第一个问题:哪些类型需要深拷贝?

我曾经在一个知名博主的一篇博客上看到一个论点,表示很疑惑。

因为我不记得在哪篇文章上有看到说包括9种类型(8种基本类型加上String类型),都不需要深拷贝 ,验证一下就知道了。

        System.out.println("正版的猴子的姓名: " + monkey.getName());
        System.out.println("复制的猴子的姓名: " + copyMonkey.getName());
        monkey.changeName();
        System.out.println("正版猴子姓名变成了" + monkey.getName());
        System.out.println("复制的猴子的姓名: " + copyMonkey.getName());

控制台打印

查了一下资料,发现String 不改变值是浅copy。但给name赋值时(set方法等),常量值的地址是固定不变的,字符串对象只能改变自己的引用了,原来对象的引用不会变,效果上这是相当于实现了深copy。

所以这里真正的结论是:8种基本类型加上String类型,都可以不需要深拷贝,底层自动会进行深拷贝。

抛出第二个问题:实现深拷贝的方法?

据我所知有两种,分别测试一下把。

第一种方法:将需要深拷贝的引用对象再次调用clone方法

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Monkey monkey =(Monkey) super.clone();
        monkey.weapon = (Weapon) this.weapon.clone();
        return monkey;
    }

这里将Monkey的clone方法改造了一下,所以Weapon中也需要加入clone方法。

/**
 * @description:
 * @author: linyh
 * @create: 2018-11-05 16:27
 **/
public class Weapon implements Cloneable{

    private int size;

    public Weapon(int size) {
        this.size = size;
    }

    public int getSize() {
        return size;
    }

    public void changeSize(){
        size++;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试一下

这里的结果就符合了我们预期的效果了。

第二种方法:序列化再反序列化

改变一下Monkey的clone方法,此方法需要将需要深拷贝的类实现Serializable接口(Monkey、Weapon)

    @Override
    protected Object clone() throws CloneNotSupportedException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;

        try {
            //序列化
            oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            //反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bis);
            return ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            try {
                oos.close();
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

这里给Monkey多加一个属性以及get和change方法便于测试。

    private List<Integer> list;

    public void changeList(){ list.add(2); }

测试类

        Monkey monkey = new Monkey();
        Monkey copyMonkey = (Monkey)monkey.clone();
        System.out.println("两个对象是否一样: " + (monkey == copyMonkey));
        System.out.println("正版猴子的各个属性: " + monkey.getName() +"," + monkey.getAge());
        System.out.println("复制猴子的各个属性: " + copyMonkey.getName() +"," + copyMonkey.getAge());
        System.out.println("两个猴子的武器是否一样: " + (monkey.getWeapon() == copyMonkey.getWeapon()));
        System.out.println("两个猴子的List是否一样: " + (monkey.getList() == copyMonkey.getList()));

        System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
        System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
        monkey.getWeapon().changeSize();
        System.out.println("正版猴子武器SIZE改变成了" +monkey.getWeapon().getSize());
        System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());

        System.out.println("正版的猴子的ListSize: " + monkey.getList().size());
        System.out.println("复制的猴子的ListSize: " + copyMonkey.getList().size());
        monkey.changeList();
        System.out.println("正版猴子ListSize改变成了" +monkey.getList().size());
        System.out.println("复制的猴子的ListSize: " + copyMonkey.getList().size());

控制台打印

这里就完成了深拷贝过程,对比两种方法,后者,只需要序列化与反序列化以及实现序列化接口即可,如果是第一种方法,需要将所有的需要深拷贝的引用都调用其上的clone方法,对比而言,像上面一个例子既有list还有weapon这种多个需要深拷贝引用的,个人觉得还是使用第二种序列化的方式比较便捷,如果只有一个引用像第一种的例子只有一个weapon需要深拷贝,就可以考虑weapon也实现一个clone,然后在monkey调用clone的时候也把weapon clone一下,毕竟只有一个。

Clone(原型模式)的优点

  1. clone方法底层原理是JVM直接复制内存块的操作,所以在速度上比直接new来的快。
  2. 初始化过程比较复杂,类中属性复杂且需要复制时,使用原型模式更快捷,不需要一个个设置属性。

 Clone(原型模式)的缺点

从上面的例子也可以看出来,需要去实现一个Cloneable接口以及其clone方法,如果需要克隆的类中有需要深拷贝类型的还需要在clone额外下功夫,比较繁琐,代码量需要增加不少。现实中我们可以在一个类定义好clone方法,需要克隆的类都可以去继承这个类,也是一个节省代码量的做法。

由于这里的原型模式clone方法并不是平时new对象的操作,所以在克隆的时候不会再执行构造器的方法,而是直接复制内存块,所以要注意的是构造器的方法克隆时不会执行,这里不做测试,感兴趣可以自己在构造器中加入输出语句方法试一下。

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/83751687