设计模式之原型模式
1.原型模式概述
原型模式 (Prototype Pattern) 是用于创建重复的对象,同时又能保证性能。属于创建型模式
,它提供了一种创建对象的最佳方式。
原型模式使用Java实现比较简单,只需要实现一个接口Cloneable,并重写接口中的 clone() 方法,就完成了原型模式。
2.代码实现
2.1实现原型模式
import java.util.List;
/*
* 为了减少代码量,使用了lombok自动生成代码。
*/
@Data //生成set/get方法
@ToString //重写toString()
@NoArgsConstructor //无参构造器
@AllArgsConstructor //全参构造器
//第一步:实现Cloneable接口
public class Student implements Cloneable {
private int age;
private char gender;
private List<Integer> scores;
/*
* 第二步,重写clone方法,直接调用父类(Object)的clone()方法。
* Object内部的clone()方法是native修饰的。
*/
@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
}
2.2测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
List<Integer> scores = new ArrayList<>();
scores.add(1);
scores.add(2);
scores.add(3);
//原始对象
Student student = new Student(14, '男', scores);
//调用clone()方法,生成一个克隆对象
Student cloneStudent = student.clone();
//两个对象内部的信息完全相同
System.out.println("原student信息: " + student);
System.out.println("克隆的student: " + cloneStudent);
//这里是false,说明克隆对象与原对象在堆中是两个不同的对象。
System.out.println("克隆的对象和原对象是否是同一个对象: " + (cloneStudent == student));
// --- 下面我们修改一下克隆对象内部的一些属性,看一下会不会对原对象产生影响。
//修改基本数据(int)类型属性 age
cloneStudent.setAge(3);
//修改引用类型(List)属性
List<Integer> cscores = cloneStudent.getScores();
cscores.add(4);
System.out.println("修改克隆对象后,原student信息: " + student);
System.out.println("修改克隆对象后,cloneStudet的信息: " + cloneStudent);
//true
System.out.println("原对象与克隆对象的scores(List)地址是否相同 : " + (student.getScores() == cloneStudent.getScores()));
}
}
/*
原student信息: Student(age=14, gender=男, scores=[1, 2, 3])
克隆的student: Student(age=14, gender=男, scores=[1, 2, 3])
克隆的对象和原对象是否是同一个对象: false
//修改基本属性类型对原对象没有影响,但是修改内部的引用类型变量会影响原对象。
修改克隆对象后,原student信息: Student(age=14, gender=男, scores=[1, 2, 3, 4])
修改克隆对象后,cloneStudet的信息: Student(age=3, gender=男, scores=[1, 2, 3, 4])
//两个内部的List属性引用的对象是同一个
原对象与克隆对象的scores(List)地址是否相同 : true
*/
2.3总结
- 当克隆完成时,实际上是在堆中新创建了一个对象,即克隆对象与原对象在堆中的地址不同。
- 如果原对象内部有引用类型的变量(如上面的List),那么当进行克隆时,只会将原对象内部的引用类型对象的
地址
赋值给克隆对象内部的引用变量,即上面的原对象与克隆对象两者的list属性地址相同,而不会真正的在堆中创建一个List出来,这种情况被称为浅拷贝
。
3.深拷贝与浅拷贝
3.1什么是深拷贝与浅拷贝
- 数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
- 浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:
只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变
。 - 深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,
在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象
。 - 深拷贝相比于浅拷贝速度较慢并且花销较大。
3.2实现深拷贝
浅拷贝通过Cloneable接口实现,深拷贝通过序列化(Serializable)
的方式实现。
/*
* 注意要实现Serializable接口。
*/
public class Student implements Serializable {
private int age;
private char gender;
private List<Integer> scores;
/*
* 自定义深拷贝方法。
* */
public Student 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);
Student Student = (Student) ois.readObject();
return Student;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
List<Integer> scores = new ArrayList<>(Arrays.asList(1, 2, 3));
Student student = new Student(1, '男', scores);
Student cloneStudent = student.deepClone();
//false
System.out.println(student == cloneStudent);
//false 深拷贝成功!!!
System.out.println(student.getScores() == cloneStudent.getScores());
}
}
原对象内部的list内存地址与克隆后对象内部的list地址不同,说明深拷贝成功!!!