声
提示:标题序号从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
中的内存地址是不同的,深拷贝成功!