什么是原型模式?
原型模式即使用已经创建的对象作为原型,通过复制该原型对象得到一个和原型完全相同的新对象。
具体结构
原型模式包含以下几种角色:
- 抽象原型类,规定了具体原型类必须实现的clone()方法;
- 具体原型类,实现了抽象原型类的clone()方法,表示此类可以被复制(克隆);
- 访问类,使用具体原型类的clone()方法来创建新的对象。
实现方法
原型模式有两种实现方法,一种是浅拷贝 ,另外一种是**深拷贝,**也另有说法为浅克隆和深克隆。
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原
有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
由上面的定义可知,浅拷贝对象中,非基本数据类型(对象类型)指向的是原有对象的内存地址,这样会导致修改其中一个对象的非基本数据类型时,另外的一个对象中的相关类型属性也会跟随改变。针对这一点需要特别注意。
Java中使用Object类中native clone()方法来实现浅拷贝,使用方式是实现Cloneable接口,重写clone()方法,如果不重写方法,由于访问权限的问题,运行会报以下错误。
java: clone() 在 java.lang.Object 中是 protected 访问控制
如果不实现Cloneable接口,会报CloneNotSupportedException异常错误。
所以必须重写clone()方法,修改访问修饰符为public,返回值为具体对象类型。
浅拷贝
以下为浅拷贝的实现代码
public class MyPrototype implements Cloneable {//实现Cloneable接口
public MyPrototype() {}
// 需要重写clone方法
@Override
public MyPrototype clone() {
try {
return (MyPrototype) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
深拷贝
深拷贝有两种实现方式,一是针对类内部的所有非基本数据类型(不包含String类型)的数据,让其实现Cloneable接口,重写其clone()方法。另外一种是使用序列化的方式,把对象先序列化,然后再反序列化回来就可以得到内存隔离的两个对象。这样修改各个对象的数据就能到达互补影响的效果
以下为示例
下面定义了Student和Teacher类,作为测试。
//定义Student类
public class Student implements Serializable {
private String studentName;
private int studentId;
public Student(String studentName, int studentId) {
this.studentName = studentName;
this.studentId = studentId;
}
public Student() {
}
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public int getStudentId() {
return studentId;
}
public void setStudentId(int studentId) {
this.studentId = studentId;
}
@Override
public String toString() {
return "Student{" +
"studentName='" + studentName + '\'' +
", studentId=" + studentId +
'}';
}
}
//定义Teacher类
public class Teacher implements Cloneable, Serializable {
private String teacherName;
private int teacherId;
private List<Student> students;
public Teacher() {
}
public Teacher(String teacherName, int teacherId, List<Student> students) {
this.teacherName = teacherName;
this.teacherId = teacherId;
this.students = students;
}
public String getTeacherName() {
return teacherName;
}
public void setTeacherName(String teacherName) {
this.teacherName = teacherName;
}
public int getTeacherId() {
return teacherId;
}
public void setTeacherId(int teacherId) {
this.teacherId = teacherId;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
@Override
public String toString() {
return "Teacher{" +
"teacherName='" + teacherName + '\'' +
", teacherId=" + teacherId +
", students=" + students +
'}';
}
@Override
public Teacher clone() {
try {
Teacher clone = (Teacher) super.clone();
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
浅拷贝存在的问题,就是修改对象中的非基本数据类型,克隆的对象也会跟随改变,示例如下。
public class PrototypeTest {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.setTeacherName("李明");
teacher.setTeacherId(1001);
List<Student> list = new ArrayList<>();
list.add(new Student("小红", 230001));
list.add(new Student("小亮", 230002));
teacher.setStudents(list);
Teacher newTeacher = teacher.clone();
newTeacher.getStudents().get(0).setStudentName("小冰");
System.out.println(newTeacher);
System.out.println(teacher);
}
}
// 结果如下:
// Teacher{teacherName='李明', teacherId=1001,
//students=[Student{studentName='小冰', studentId=230001},
//Student{studentName='小亮', studentId=230002}]}
// Teacher{teacherName='李明', teacherId=1001,
//students=[Student{studentName='小冰', studentId=230001},
// Student{studentName='小亮', studentId=230002}]}
// 测试结果显示,修改teacher对象后,newTeacher中的数据也一同发生了变化。
// 这是因为Student数据指向了同一块内存区域。
解决办法一
一是使用序列化的方式,把对象先序列化,然后再反序列化回来就可以得到内存隔离的两个对象。这样修改各个对象的数据就能到达互补影响的效果。
public class PrototypeTest {
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher();
teacher.setTeacherName("李明");
teacher.setTeacherId(1001);
List<Student> list = new ArrayList<>();
list.add(new Student("小红", 230001));
list.add(new Student("小亮", 230002));
teacher.setStudents(list);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("teacher.ob"));
objectOutputStream.writeObject(teacher);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("teacher.ob"));
Teacher newTeacher1 = (Teacher) objectInputStream.readObject();
newTeacher1.getStudents().get(0).setStudentName("小梅");
teacher.getStudents().get(0).setStudentName("小美");
System.out.println(teacher);
System.out.println(newTeacher1);
System.out.println(teacher == newTeacher1);
}
}
// 测试结果
// Teacher{teacherName='李明', teacherId=1001,
// students=[Student{studentName='小美', studentId=230001},
// Student{studentName='小亮', studentId=230002}]}
// Teacher{teacherName='李明', teacherId=1001,
// students=[Student{studentName='小梅', studentId=230001},
// Student{studentName='小亮', studentId=230002}]}
可以看到,使用序列化和反序列化的方式后,得到的对象是内存隔离的,修改各个对象的数据互不影响。
解决办法二
针对类内部的所有非基本数据类型(不包含String类型)的数据,让其实现Cloneable接口,重写其clone()方法。
所以需要让Student 类实现Cloneable接口,重写其clone方法。Teacher类也需要重写其clone方法,针对非基本类型也要调用clone方法,代码如下:
public class Student implements Cloneable {
private String studentName;
private int studentId;
public Student(String studentName, int studentId) {
this.studentName = studentName;
this.studentId = studentId;
}
public Student() {
}
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public int getStudentId() {
return studentId;
}
public void setStudentId(int studentId) {
this.studentId = studentId;
}
@Override
public String toString() {
return "Student{" +
"studentName='" + studentName + '\'' +
", studentId=" + studentId +
'}';
}
@Override
public Student clone() {
try {
Student clone = (Student) super.clone();
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Teacher类clone方法重写
package com.code.designpattern.prototype;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Teacher implements Cloneable{
private String teacherName;
private int teacherId;
private List<Student> students;
public Teacher() {
}
public Teacher(String teacherName, int teacherId, List<Student> students) {
this.teacherName = teacherName;
this.teacherId = teacherId;
this.students = students;
}
public String getTeacherName() {
return teacherName;
}
public void setTeacherName(String teacherName) {
this.teacherName = teacherName;
}
public int getTeacherId() {
return teacherId;
}
public void setTeacherId(int teacherId) {
this.teacherId = teacherId;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
@Override
public String toString() {
return "Teacher{" +
"teacherName='" + teacherName + '\'' +
", teacherId=" + teacherId +
", students=" + students +
'}';
}
@Override
public Teacher clone() {
try {
// 重点在于改写这个clone方法,对非基本数据类型都需要调用clone方法重新赋值。
Teacher clone = (Teacher) super.clone();
List<Student> studentsList = clone.getStudents();
List<Student> studentsList2 = new ArrayList<>();
for (Student s:studentsList) {
studentsList2.add(s.clone());
}
clone.setStudents(studentsList2);
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
查看最终效果如下:
public class PrototypeTest {
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher();
teacher.setTeacherName("李明");
teacher.setTeacherId(1001);
List<Student> list = new ArrayList<>();
list.add(new Student("小红", 230001));
list.add(new Student("小亮", 230002));
teacher.setStudents(list);
Teacher newTeacher = teacher.clone();
newTeacher.getStudents().get(0).setStudentName("小冰");
System.out.println(newTeacher);
System.out.println(teacher);
}
}
// 测试输出如下:
//Teacher{teacherName='李明', teacherId=1001,
//students=[Student{studentName='小冰', studentId=230001},
//Student{studentName='小亮', studentId=230002}]}
//Teacher{teacherName='李明', teacherId=1001,
//students=[Student{studentName='小红', studentId=230001},
//Student{studentName='小亮', studentId=230002}]}
总结:
一般推荐使用序列化的方法进行深拷贝,实现Cloneable接口重写clone方法的方式有些繁琐。