初学Java常用设计模式之——原型模式

123 阅读3分钟

声明:转载请附上原文链接

提示:标题序号从3开始,是照应不同设计模式笔记发布的顺序而定的,比如,第上一篇文章 初学Java常用设计模式之——工厂模式 序号从2开始。

标题后面之所以加上了解,是因为相对于单例模式,工厂模式来说原型模式用的比较少,但原型模式的深拷贝和浅拷贝是需要了解一下的!

3. 原型模式(了解)

3.1 原型模式介绍

  • 原型设计模式Prototype

    • 是一种对象创建型模式,使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,主要用于创建重复的对象,同时又能保证性能
    • 工作原理是将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程
    • 应该是最简单的设计模式了,实现一个接口,重写一个方法即完成了原型模式
  • 核心组成

    • Prototype: 声明克隆方法的接口,是所有具体原型类的公共父类,Cloneable接口
    • ConcretePrototype : 具体原型类
    • Client: 让一个原型对象克隆自身从而创建一个新的对象
  • 应用场景

    • 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得
    • 如果系统要保存对象的状态,做备份使用

3.2 原型模式案例

首先我们来创建一个具体原型类 Person.java 并让其实现 Cloneable 接口,重写clone() 方法:

/**
 * @Auther: csp1999
 * @Date: 2020/11/08/7:31
 * @Description: Person 具体原型类实现 Cloneable 接口,能被克隆
 */
public class Person implements Cloneable{

    private String name;
    private int age;

    public Person(){
        System.out.println("空参构造函数调用...");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    /**
     * 重写克隆方法,返回Person对象类型
     *
     * 注意:权限改成public,方便调用
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

在测试类中调用并打印结果:

@Test
public void testPropotype() throws CloneNotSupportedException {
    Person person1 = new Person();
    person1.setAge(22);
    person1.setName("csp");
    System.out.println(person1);
    Person person2 = person1.clone();
    person2.setName("hzw");
    System.out.println(person2);
}

结果如下:

空参构造函数调用...
Person{name='csp', age=22}
Person{name='hzw', age=22}

从结果可以看出:person2 是 person1通过复制后得来的,二者数据内容相同。但需要注意的是,person1调用clone();方法得到person2,并没有经过Person 类中的空参构造函数,因此打印结果只输出一次空参构造函数调用...

接下来,我们在Person 在加上新的复杂数据类型的成员变量:List

private List<String> list;

再来测试:

@Test
public void testPropotype() throws CloneNotSupportedException {
    Person person1 = new Person();
    person1.setAge(22);
    person1.setName("csp");
    // 初始化list 并为其加入数据
    person1.setList(new ArrayList<>());
    person1.getList().add("aaa");
    person1.getList().add("bbb");
    System.out.println("person1:"+person1);
    Person person2 = person1.clone();
    person2.setName("hzw");
    // 给peron2 中的list添加一条数据
    person2.getList().add("ccc");
    System.out.println("person2"+person2);
    System.out.println("person1:"+person1);
    boolean flag1 = person1 == person2;
    System.out.println("person1 和 person2 的 引用地址是否相同: " +  flag1);
    boolean flag2 = person1.getList() == person2.getList();
    System.out.println("person1 和 person2 的 list 引用地址是否相同: " +  flag2);
}

输出结果:

空参构造函数调用...
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person2Person{name='hzw', age=22, list=[aaa, bbb, ccc]}
person1:Person{name='csp', age=22, list=[aaa, bbb, ccc]}
person1 和 person2 的 引用地址是否相同: false
person1 和 person2 的 list 引用地址是否相同: true

由结果可以看出:

  • 当克隆执行完成后,实际上相当于新 new 一个Person 对象并为其分配了新的存储地址及引用,因此person1 和 person2 的地址引用不同;
  • 而对于Person 对象的复杂类型成员变量 list,当执行克隆的时候,实际上是从被拷贝对象person1 中 拷贝了list 的引用地址给person2 中的 list,而并非新new(创建)一个 List 出来;
  • 因此二者其实是共享一个相同地址引用的list,所以person1.getList() == person2.getList();true,这也就说明了,为什么在 person2的list 中添加数据ccc时,person1 中的list也添加了ccc,而这种情况就被称为 浅拷贝;

那么如何解决浅拷贝的问题呢?请接着往下阅读!

3.1 原型模式深拷贝/浅拷贝

  • 遗留问题:

    • 通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的

    • 浅拷贝实现 Cloneable,深拷贝是通过实现 Serializable 读取二进制流

    • 拓展

      • 浅拷贝:

        如果原型对象的成员变量是基本数据类型(int、double、byte、boolean、char等),将复制一份给克隆对象;
        如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,
        也就是说原型对象和克隆对象的成员变量指向相同的内存地址
        
        通过覆盖Object类的clone()方法可以实现浅克隆
        
      • 深拷贝:

        无论原型对象的成员变量是基本数据类型还是引用类型,都将复制一份给克隆对象,如果需要实现深克隆,可以通过序列化(Serializable)等方式来实现
        
  • 优点

    • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,可以提高新实例的创建效率
    • 可辅助实现撤销操作,使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用恢复到历史状态
  • 缺点

    • 需要为每一个类配备一个克隆方法,对已有的类进行改造时,需要修改源代码,违背了“开闭原则”
    • 在实现深克隆时需要编写较为复杂的代码,且当对象之间存在多重的嵌套引用时,需要对每一层对象对应的类都必须支持深克隆

深拷贝实现:

首先Person 对象实现 Serializable 接口,然后自定义深拷贝方法 deepClone()

/**
 * 深拷贝
 * 
 * 注意:要实现序列化接口
 * @return
 */
public Person deepClone() {
    try {
        // 输出 (序列化)
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(this);
        // 输入 (反序列化)
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Person person = (Person) ois.readObject();
        return person;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

接下来验证一下深拷贝是否成功:

@Test
public void testPropotype() throws CloneNotSupportedException {
    Person person1 = new Person();
    person1.setAge(22);
    person1.setName("csp");
    // 初始化list 并为其加入数据
    person1.setList(new ArrayList<>());
    person1.getList().add("aaa");
    person1.getList().add("bbb");
    System.out.println("person1:"+person1);
    //-----------------------------浅拷贝-------------------------------
    //Person person2 = person1.clone();
    //-----------------------------深拷贝-------------------------------
    Person person2 = person1.deepClone();
    person2.setName("hzw");
    // 给peron2 中的list添加一条数据
    person2.getList().add("ccc");
    System.out.println("person2"+person2);
    System.out.println("person1:"+person1);
    boolean flag1 = person1 == person2;
    System.out.println("person1 和 person2 的 引用地址是否相同: " +  flag1);
    boolean flag2 = person1.getList() == person2.getList();
    System.out.println("person1 和 person2 的 list 引用地址是否相同: " +  flag2);
}

输出结果:

空参构造函数调用...
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person2Person{name='hzw', age=22, list=[aaa, bbb, ccc]}
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person1 和 person2 的 引用地址是否相同: false
person1 和 person2 的 list 引用地址是否相同: false

由结果可得出:深拷贝 person2 所得到的 list 内存地址和原来person1 中的内存地址是不同的,深拷贝成功!

之后我会陆续更新其他设计模式博文,如果文章对您有帮助,希望点个赞/收藏/关注! O(∩_∩)O~