原型模式
概述
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
结构
原型模式包含如下角色:
- 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
类图
原型模式的克隆分为浅克隆和深克隆。
浅克隆
:创建一个新对象,新对象的属性
和原来对象完全相同
,对于非基本类型属性
,仍指向原有
属性所指向的对象的内存地址
。
深克隆
:创建一个新对象,属性中引用的其他对象也会被克隆
,不再
指向原有对象地址
。
Java中的Object类中提供了 clone()
方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable
接口的子实现类就是具体的原型类。代码如下:
类图实现
浅克隆
UserPrototype(具体的原型类):
package com.lx.design.creator.prototype;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 15:11
* 用户
*/
public class UserPrototype implements Cloneable{
public UserPrototype() {
System.out.println("原型对象创建成功");
}
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected UserPrototype clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (UserPrototype) super.clone();
}
}
Client测试类
package com.lx.design.creator.prototype;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 15:29
*/
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
UserPrototype r1 = new UserPrototype();
UserPrototype r2 = r1.clone();
System.out.println(" 原型对象r1和克隆对象r2是否是同一个对象?" + (r1 == r2));
}
}
输出结果为false,为什么呢?
当调用 r1.clone() 时,它会创建并返回一个新的对象,这个新对象是通过复制 r1 的字段而创建的。虽然这两个对象可能包含相同的数据,但它们在内存中是不同的对象实例。
如果希望 r1 == r2 返回 true
需要在 UserPrototype 类中重写 clone() 方法,确保实现深拷贝。深拷贝会递归地复制对象及其所有引用的对象,从而确保两个对象是彻底独立的,而不仅仅是引用地址不同。
r1.clone();是怎么创建对象的呢?
通过上面的案例。看出控制台只打印一次
原型对象创建成功
。说明clone()不是通过构造方法进行创建对象的。它是通过内存复制的方式直接创建对象。这也是为什么 r2 和 r1 是不同的对象,尽管它们的内容相同。
用原型模式生成“三好学生”奖状
同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,可以使用原型模式复制多个“三好学生”奖状出来,然后在修改奖状上的名字即可。
Citation
package com.lx.design.creator.prototype.citation;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 16:38
*/
public class Citation implements Cloneable {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return (this.name);
}
public void show() {
System.out.println(name + "同学: 被评为三好学生");
}
@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
CitationTest
package com.lx.design.creator.prototype.citation;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 16:39
*/
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
c1.setName("张三");
//复制奖状
Citation c2 = c1.clone();
//将奖状的名字修改李四
c2.setName("李四");
c1.show();
c2.show();
}
}
输出结果为
张三同学: 被评为三好学生
李四同学: 被评为三好学生
问题 为什么c2.setName(“李四”);设置之后,c1和c2不应该都是李四吗?
对象的基本类型属性值是基于值传递的,而不是引用传递。即使在原型模式中通过克隆对象,对象的字段仍然是独立的,修改一个对象的字段值不会影响另一个对象相同字段的值。这就是为什么你看到的现象是 c1 的值没有因为 c2.setName(“李四”);而改变的原因。
让我们详细解释一下:
String 是不可变的:
在Java中,String 对象是不可变的,也就是说一旦创建,它的值就不能被修改。当你调用setName()
方法时,它实际上是创建了一个新的 String
对象,并将这个新对象的引用赋给了 name 字段。这不会影响原来 String 对象的值,因为它们是不可变的。
使用场景
- 对象的创建非常复杂,可以使用原型模式快捷的创建对象。
- 性能和安全要求比较高。
深克隆
Student
package com.lx.design.creator.prototype.citation;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 17:52
*/
public class Student {
private String name;
private String address;
public Student() {
}
public Student(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
Citation
package com.lx.design.creator.prototype.citation;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 16:38
*/
public class Citation implements Cloneable {
private Student stu;
private String age;
public Student getStu() {
return stu;
}
public void setStu(Student stu) {
this.stu = stu;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
void show() {
System.out.println(stu.getName() + "同学:终于在"+age+"岁,进阶元婴");
}
@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
CitationTest
package com.lx.design.creator.prototype.citation;
/**
* TODO 添加描述
*
* @author wangLJ
* @date 2024/6/17 16:39
*/
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setAge("450");
c1.setStu(stu);
//复制奖状
Citation c2 = c1.clone();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
c2.setAge("360");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
}
输出结果为:
stu和stu1是同一个对象?true
李四同学:终于在450岁,进阶元婴
李四同学:终于在360岁,进阶元婴
问题分析
在这段代码中,虽然通过
浅拷贝
的方式复制了Citation对象 c1
生成了c2
,但是在复制过程中,只是简单地复制了基本数据类型和引用类型的地址,并没有对引用类型的对象进行深度复制。所以在修改c2
中的学生对象名字
时,实际上是修改了同一个学生对象的属性
,因此导致最后输出的两个奖状对象的学生姓名都变成了 “李四
”。
因此,尽管c1
和c2
是不同的Citation
对象,但它们引用
的学生对象
仍然是同一个
,所以 stu 和 stu1 是同一个对象。
c1和c2中的age 更改成功,是因为基本类型是直接值传递,只复制值,所以是不影响的。
实现深克隆
序列化
Citation 和Student 需要实现Serializable
public static void main(String[] args) throws Exception {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\b.txt"));
//将c1对象写出到文件中
oos.writeObject(c1);
oos.close();
//创建对象出入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\b.txt"));
//读取对象
Citation c2 = (Citation) ois.readObject();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
从序列化和反序列化可以看出,深克隆就是把对象转换成文件从而再转换出来。那么我们也可以把对象转换成JSON, 在转换为对象,也是可以完成对象的深克隆, 知道理念之后。可以有很多种深克隆的办法
- 使用 Apache Commons Lang 的 SerializationUtils
Student stu = new Student("张三", "西安");
Citation c1 = new Citation(stu);
// 深克隆
Citation c2 = SerializationUtils.clone(c1);
Student stu1 = c2.getStu();
stu1.setName("李四");
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
- 使用 Gson
Gson gson = new Gson();
Student stu = new Student("张三", "西安");
Citation c1 = new Citation(stu);
// 深克隆
String json = gson.toJson(c1);
Citation c2 = gson.fromJson(json, Citation.class);
Student stu1 = c2.getStu();
总结
原型模式是一种创建型设计模式
,它允许通过复制
现有对象来创建新
对象,而不是
通过实例化类
。这样可以避免耗时
的实例化操作,提高性能。以下是原型模式的优缺点:
优点:
- 减少对象的创建时间:通过复制现有对象来创建新对象,避免了耗时的实例化操作,提高了对象创建的效率。
- 简化对象的创建过程:无需关心对象的具体创建过程,只需要根据现有对象进行复制即可,简化了对象的创建过程。
- 动态添加或删除对象:可以动态地添加或删除原型,方便灵活地管理对象。
缺点:
- 对象复杂时性能下降:如果对象比较复杂,包含很多属性和关联关系,复制对象可能会导致性能下降。
- 深拷贝与浅拷贝问题:在复制对象时需要考虑深拷贝和浅拷贝的问题,确保复制的对象完整性。
难以理解:原型模式相对于其他创建型模式(如工厂模式、建造者模式)更加抽象,可能会增加代码的复杂度和难度。
总的来说,原型模式适用于需要频繁创建相似对象,并且对象的创建过程比较复杂或耗时的情况下,能够提高效率和简化代码。