魔鬼还是天使的博客

魔鬼还是天使的博客

「补课」进行时:设计模式(6)——郭靖大侠带你学原型模式-LMLPHP

1. 前文汇总

「补课」进行时:设计模式系列

2. 找工作

这一天,郭靖大侠因为在桃花岛调戏侍女被黄蓉打出了桃花岛,这下可玩大了,从桃花岛被赶出来吃啥喝啥啊,得赶紧找份工作,西北风可喝不饱肚子哇~~~

这不,我们的郭大侠就开始写简历,准备向丐帮、全真教、白驼山和段氏家族投一份简历,看看能不能先混碗饭吃,等老婆的气消了再回去。

首先,先定义一个简历类:

public class Resume {
    private String name;
    private String position;
    private int salary;

    // 省略 get/set

    @Override
    public String toString() {
        return "Resume{" +
                "name='" + name + '\'' +
                ", position='" + position + '\'' +
                ", salary=" + salary +
                '}';
    }
}

然后,我们的郭大侠开始了熬夜写简历的生活:

public class Test {
    public static void main(String[] args) {
        Resume resume1 = new Resume();
        resume1.setName("小郭");
        resume1.setPosition("一代大侠");
        resume1.setSalary(1000);
        System.out.println(resume1);

        Resume resume2 = new Resume();
        resume2.setName("小郭");
        resume2.setPosition("一代大侠");
        resume2.setSalary(1200);
        System.out.println(resume2);

        Resume resume3 = new Resume();
        resume3.setName("小郭");
        resume3.setPosition("一代大侠");
        resume3.setSalary(1500);
        System.out.println(resume3);

        // ...
}

简历这么一份一份的写太累了,工作都没找到可能先饿死了,不行,小郭同学需要提高写简历的效率,于是,他去找了一个打印机回来:

public class Test {
    public static void main(String[] args) {
        // 效率倍增,直接循环开始写简历
        for (int i = 0; i < 5; i++) {
            Resume resume4 = new Resume();
            int salary = (int)(1000 + Math.random() * (2000 - 1000 + 1));
            resume4.setName("小郭");
            resume4.setPosition("一代大侠");
            resume4.setSalary(salary);
            System.out.println(resume4.toString());
        }
    }
}

这个时候,感觉效率好像还是有点低,每次只能一张一张打印,浪费时间,于是乎,我们的郭大侠又去搞了一个复印机回来。

可是使用复印机需要我们原本的简历支持这个功能,听过这个功能需要扩展 Cloneable 接口:

public class ResumeClone implements Cloneable {
    private String name;
    private String position;
    private int salary;
    // 省略 get/set
    @Override
    protected ResumeClone clone(){
        ResumeClone resumeClone = null;
        try{
            resumeClone = (ResumeClone) super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return resumeClone;
    }

    @Override
    public String toString() {
        return "ResumeClone{" +
                "name='" + name + '\'' +
                ", position='" + position + '\'' +
                ", salary=" + salary +
                '}';
    }
}

然后我们的复印机就能跑起来了:

public class TestClone {
    public static void main(String[] args) {
        int num = 5;
        ResumeClone resumeClone = new ResumeClone();
        while (num > 0){
            ResumeClone resume1 = resumeClone.clone();
            int salary = (int)(1000 + Math.random() * (2000 - 1000 + 1));
            resume1.setName("小郭");
            resume1.setPosition("一代大侠");
            resume1.setSalary(salary);
            System.out.println(resume1.toString());
            num --;
        }
    }
}

这里实际上我们只有第一个对象是使用打印机打印出来的,后面的对象都是通过复印机直接复印出来的。

这其实就是设计模式中的原型模式。

3. 原型模式

原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单,使用的场景才非常地多,其定义如下:

Specify the kinds of objects to create using a prototypical instance,andcreate new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。)

「补课」进行时:设计模式(6)——郭靖大侠带你学原型模式-LMLPHP

这个绝对是最简单的设计模式,整个模式的核心就只有一个 clone 方法,通过该方法进行对象的拷贝, Java 提供了一个 Cloneable 接口来标示这个对象是可拷贝的,为什么说是「标示」呢?翻开 JDK 的帮助看看 Cloneable 是一个方法都没有的,这个接口只是一个标记作用,在 JVM 中具有这个标记的对象才有可能被拷贝。那怎么才能从「有可能被拷贝」转换为「可以被拷贝」呢?方法是覆盖 clone() 方法。

通用代码:

public class PrototypeClass implements Cloneable{
    @Override
    protected PrototypeClass clone() {
        PrototypeClass prototypeClass = null;
        try {
            prototypeClass = (PrototypeClass) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return prototypeClass;
    }
}

优点:

  1. 性能优良

原型模式是在内存二进制流的拷贝,要比直接 new 一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。

  1. 逃避构造函数的约束

这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束。

4. 构造函数

先看一个简单的有关构造函数的示例:

public class ConstructorDemo implements Cloneable {
    public ConstructorDemo() {
        System.out.println("我被执行了。。。");
    }

    @Override
    protected ConstructorDemo clone(){
        ConstructorDemo demo = null;
        try {
            demo = (ConstructorDemo) super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return demo;
    }
}

public class ConstructorTest {
    public static void main(String[] args) {
        ConstructorDemo demo = new ConstructorDemo();
        ConstructorDemo demo1 = demo.clone();
    }
}

执行结果如下:

我被执行了。。。

就输出一次,这里可以证明对象拷贝的时候构造函数是不会执行的,原因在于拷贝是直接在堆中进行,这其实也可以理解, new 的时候, JVM 要走一趟类加载流程,这个流程非常麻烦,在类加载流程中会调用构造函数,最后生成的对象会放到堆中,而拷贝就是直接拷贝堆中的现成的二进制对象,然后重新一个分配内存块。

5. 浅拷贝和深拷贝

先看一个浅拷贝的案例:

public class ShallowCopy implements Cloneable {
    private ArrayList<String> array = new ArrayList<> ();
    @Override
    public ShallowCopy clone() {
        ShallowCopy copy = null;
        try {
            copy = (ShallowCopy) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return copy;
    }

    public void setValue(String value) {
        this.array.add(value);
    }

    public ArrayList<String> getValue() {
        return this.array;
    }
}

public class ShallowCopyTest {
    public static void main(String[] args) {
        ShallowCopy copy = new ShallowCopy();
        copy.setValue("123");
        ShallowCopy copy1 = copy.clone();
        copy1.setValue("456");
        System.out.println(copy.getValue());
    }
}

执行的结果是:

[123, 456]

这种情况就是浅拷贝, Java 只拷贝你指定的对象,至于你指定的对象里面的别的对象,它不拷贝,还是把引用给你,共享变量,这是一种非常不安全的方式,需要特别注意。

内部的数组和引用对象不会拷贝,其他的原始基本类型和 String 类型会被拷贝。

那么这种情况如何进行一个深拷贝呢?只需要修改一下刚才 clone 的方法:

// 深拷贝
@Override
public ShallowCopy clone() {
    ShallowCopy copy = null;
    try {
        copy = (ShallowCopy) super.clone();
        this.array = (ArrayList<String>) this.array.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    return copy;
}

还是刚才的测试类,这次的运行结果是:

[123]
11-02 11:46