什么是原型模式?
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大,在这种情况下,我们可以利用对已有对象(原型)进行复制(拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern)。使用这种方式创建新对象,就无需再通过 new 实例化来创建对象了。这是因为 Object 类的 clone 方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对 new 实例化来说会更好。
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作了。
原型模式的实现
我们先通过一个简单的例子来实现一个原型模式:
public class Prototype implements Cloneable {
public void show(){
System.out.println("原型模式实现类");
}
@Override
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
public class Client {
public static void main(String[] args){
Prototype cp = new Prototype();
for(int i=0; i< 10; i++){
Prototype clonecp = (Prototype)cp.clone();
clonecp.show();
}
}
}
要实现一个原型类,需要具备三个条件:
- 实现 Cloneable 接口:Cloneable 接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用 clone 方法。在 JVM 中,只有实现了 Cloneable 接口的类才可以被拷贝,否则会抛出 CloneNotSupportedException 异常。
- 重写 Object 类中的 clone 方法:在 Java 中,所有类的父类都是 Object 类,而 Object 类中有一个 clone 方法,作用是返回对象的一个拷贝。
- 在重写的 clone 方法中调用 super.clone():默认情况下,类不具备复制对象的能力,需要调用 super.clone() 来实现。
从上面我们可以看出,原型模式的主要特征就是使用 clone 方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是 a 和 b 对象指向了同一个内存地址,如果 b 修改了,a 的值也就跟着被修改了。我们可以通过一个简单的例子来看看普通的对象复制问题:
@Getter
@Setter
public class Student {
private String name;
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName("test1");
Student stu2 = stu1;
stu2.setName("test2");
System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}
实际上,程序输出的结果是:
学生1:test2
学生2:test2
通过 clone 方法复制的对象才是真正的对象复制,clone 方法复制的对象完全是一个独立的对象。刚刚讲过了,Object 类的 clone 方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。
@Getter
@Setter
public class Student implements Cloneable {
private String name;
@Override
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student(); //创建学生1
stu1.setName("test1");
Student stu2 = stu1.clone(); //通过克隆创建学生2
stu2.setName("test2");
System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}
这次,程序输出的结果是:
学生1:test1
学生2:test2
浅拷贝和深拷贝
学习原型模式,我们还需要了解两个概念:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。
浅拷贝和深拷贝的区别在于,浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象。相反,深拷贝不仅仅会复制指针,还会复制对象本身。浅拷贝得到的对象跟原始对象共享数据,而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比浅拷贝更加耗时、更加耗内存空间。如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。
在 Java 语言中,Object 类的 clone() 方法执行的就是浅拷贝。它只会拷贝对象中基本类型的数据(int、long)以及引用对象的内存地址,而不会递归地拷贝引用对象本身。所以,当我们在使用 clone() 方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。
@Getter
@Setter
public class Student implements Cloneable {
private String name;
private Teacher teacher;
@Override
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
@Getter
@Setter
public class Teacher implements Cloneable{
private String name;
@Override
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Teacher teacher = new Teacher (); //定义老师1
teacher.setName("刘老师");
Student stu1 = new Student(); //定义学生1
stu1.setName("test1");
stu1.setTeacher(teacher);
Student stu2 = stu1.clone(); //定义学生2
stu2.setName("test2");
stu2.getTeacher().setName("王老师");//修改老师
System.out.println("学生" + stu1.getName + "的老师是:" + stu1.getTeacher().getName);
System.out.println("学生" + stu1.getName + "的老师是:" + stu2.getTeacher().getName);
}
}
运行结果:
学生test1的老师是:王老师
学生test2的老师是:王老师
可以看到:在我们给学生 2 修改老师时,学生 1 的老师也跟着被修改了。这就是浅拷贝带来的问题。
我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型,没有引用对象为止。具体代码如下:
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();//克隆teacher对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
此外,还有一种方法进行深拷贝:先将对象序列化,然后再反序列化成新的对象。具体代码如下:
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}