原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆(复制体)。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。用原型的实例创建对象,并且通过拷贝原型创建新的对象。主要解决在运行期建立和删除原型,
讲了那么多,在什么地方使用呢?
- 当一个系统应该独立于他的产品创建,构成和表示时。
- 当要实例化类要在运行时刻指定时,例如动态装载。
- 为了避免创建一个与产品层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆。
- 利用已有的原型对象,快速生成和原型一样的实例。
优点:
- 性能提高
- 逃避构造函数的约束。
缺点:
- 配置克隆需要对类的功能进行考虑,对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
- 必须实现Cloneable接口
使用场景:
- 资源优化场景。
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
- 性能和安全要求的场景。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
温馨提示:
与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
通过上面可以看到原型模式就是浅拷贝和深拷贝,拷贝也就是复制。下面我们就来了解一下他们之间的区别。这里举例便于理解:
举例:
浅拷贝就相当于把你的外表克隆出来(直接赋值给另一个对象,但他们指向的还是一个内存地址,当一个对象修改了,另一个也会跟着修改),深拷贝就相当于昨天的你和今天的你,两个人都处于一个独立的空间,互不影响(复制一个对象到一个全新的对象,两者之间没有关联)。
Java使用new关键字创建对象的过程
一般对象的拷贝方式有三种,直接赋值、浅拷贝、深拷贝,在说这三个拷贝之前,有必要说一下new创建对象的过程:
当Java虚拟机(JVM)遇到一条new指令的时候,首先去检查这个指令的参数是否能在常量池中定位到一个类的引用符号,并且检查这个引用的符号代表是否被加载,解析和初始化过。如果没有,那必须执行相应的类加载过程。
Java堆中内存绝对规整:
在类加载通过后,接下来将虚拟机将为新生对象分配空间。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间就相当于把已经确定好的内存从Java堆中分配出来,假设Java堆中的内存是绝对规整的,用过的内存放一边,没有用过的放另一边,中间放着一个指针作为分界点的指示器。那所分配内存就是仅仅把中间的指针指向空闲内存那边挪动与对象内存大小相等的距离,这种分配称做指针碰撞(Bump the Pointer)
Java堆中内存不规整:
如果Java中的内存是不规整的,那么就会出现这样一种现象,已使用的内存和空闲的内存相互交错。这样就没办法做到上面那种指针碰撞了,虚拟机就必须维护一个列表,记录哪块内存是可用的。在分配的时候从列表中选择一块足够大的空间划分给对象。
内存分配完成后,虚拟机需要将分配的空间都初始化为零值(不包括对象头)。随后,虚拟机需要对对象进行必要的设置,例如对象是谁的实例,如何能找对象的元数据信息、对象的哈希码,对象的GC分代年龄等信息。这些信息存放在对象头(Object Header)之中。根据当前虚拟机运行的状态的不同,如是否用便向锁等,对象头会有不同的设置,在上面的工作都完成后,从虚拟机的视角来看,一个新的对象已经产生了。但从Java程序的视角来看,对象创建才刚刚开始----方法还没执行,所有字段都还为零。(所有,一般说是由字节码中是否跟随invokesprcial所决定),执行new指令后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正的对象才算产生出来,生产的对象会在栈中有一个引用地址。了解对象的创建过程,对对象拷贝有一个更好的理解。
直接赋值:
直接赋值,也就是我们平常经常用的对象赋值,类似与 Person a = new Person() ;Person b = a;这就是直接赋值,由于Java是值传递,所以使用这种方式进行赋值时,并没有生成一个新的对象,而是将对象的内存引用地址指向了新的对象,也就是堆中创建的对象没有改变,只是多了一个指向。这样看可能不太清楚,就写一段代码来看看。
源码展示:
public class Student
{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
class Main{
public static void main(String[] args)
{
Student student=new Student("张三",4);
Student student1=student;
System.out.println("student原对象的姓名"+student.getName());
System.out.println("student1新对象的姓名"+student1.getName());
System.out.println("student原对象的地址"+student);
System.out.println("student1新对象的地址"+student1);
}
}
运行结果:
student原对象的姓名 :张三
student1新对象的姓名 :张三
student原对象的地址 :javaspring.prant.sample_code.Student@70177ecd
student1新对象的地址 :javaspring.prant.sample_code.Student@70177ecd
通过运行结果可以看到student和student1操作的是一个对象,当赋值对象改变了一个属性的值,原对象也会跟着改变,看看直接赋值内存的分配的空间,就清楚了。
总之,直接赋值不会产生新的对象,只是多了一个指向。但是有时候产生一个新对象。新对象的值不会影响到原型对象。当然你可以同时new两个对象,然后相互赋值,但是属性非常多时,这种方式就会非常麻烦,这个时候就可以用Object()类中clone()方法来实现一下,也就是我们说的浅拷贝。
浅拷贝(Shallow Copy)
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
实现浅拷贝,一般有两个步骤:
- 实现(clonable)接口,这个接口又称为标记接口,Java类似这样的接口会有很多,例如:序列化接口(Serializable)、随机访问的接口RandomAccess,都是此类型的接口,这类接口没有具体的方法,作用就是起到标记的作用。如果没有实现这个接口,调用clone()方法,就会抛异常,CloneNotSupportedException。
- 重写clone(),就可以实现浅拷贝。
简单来说可以理解为浅拷贝只解决第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。
//实现Cloneable标记接口 ,实现浅拷贝
public class Student implements Cloneable
{
private int id;
private String name;
private XStudent xStudent;
public Student(int id, String name, XStudent xStudent)
{
this.id = id;
this.name = name;
this.xStudent = xStudent;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public XStudent getxStudent() {
return xStudent;
}
public void setxStudent(XStudent xStudent) {
this.xStudent = xStudent;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
static class XStudent{
private String name;
private int age;
public XStudent(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
}
写一个测试类看效果
public class Run {
public static void main(String[] args)
{
Student.XStudent xStudent=new Student.XStudent("李五",12);
//new一个对象
Student student = new Student(1, "张三",xStudent);
try {
//克隆上面的对象
Student student1= (Student) student.clone();
System.out.println("比较对象和克隆对象的内存地址,发现是不一样的");
System.out.println("原型对象的内存地址 :"+student);
System.out.println("克隆对象的内存地址 :"+student1);
System.out.println("---------------------------------------------------------");
System.out.println("通过这里可以看到指向了同一个引用 证明了原型和克隆用的是同一个引用地址");
Student.XStudent xStudent1=student.getxStudent();
Student.XStudent xStudent2=student1.getxStudent();
System.out.println("原型孩子的克隆内存地址"+xStudent1);
System.out.println("克隆孩子的内存地址"+xStudent2);
System.out.println("---------------------------------------------------------");
System.out.println("改变克隆对象的姓名,发现只有一个改变了,另一个没有改变");
student1.setName("班主任");
System.out.println("原型 :"+student.getName());
System.out.println("克隆 :"+student1.getName());
System.out.println("---------------------------------------------------------");
System.out.println("改变他们相同引用的值 发现改变任意一个 都会一起更改");
xStudent2.setName("bbb");
System.out.println("原型孩子 :"+xStudent1.getName());
System.out.println("克隆孩子 :"+xStudent2.getName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
比较对象和克隆对象的内存地址,发现是不一样的
原型对象的内存地址 :javaspring.prant.cloneable.Student@70177ecd
克隆对象的内存地址 :javaspring.prant.cloneable.Student@1e80bfe8
------------------------------------------------------------------------------------------------------------------
通过这里可以看到指向了同一个引用 证明了原型和克隆用的是同一个引用地址
孩子原型的克隆内存地址javaspring.prant.cloneable.Student$XStudent@66a29884
克隆孩子的内存地址javaspring.prant.cloneable.Student$XStudent@66a29884
------------------------------------------------------------------------------------------------------------------
改变克隆对象的姓名,发现只有一个改变了,另一个没有改变
原型 :张三
克隆 :班主任
------------------------------------------------------------------------------------------------------------------
改变他们相同引用的值 发现改变任意一个 都会一起更改
原型孩子 :bbb
克隆孩子 :bbb
通过输出结果可以得到三个重要的信息:
1 通过这个可以看到student(原型)和student1(克隆)的内存地址是不一样的
比较对象和克隆对象的内存地址,发现是不一样的
原型对象的内存地址 :javaspring.prant.cloneable.Student@70177ecd
克隆对象的内存地址 :javaspring.prant.cloneable.Student@1e80bfe8
2.通过这个可以看到改变改变了克隆对象的值,原型对象没有改变,String不也是引用类型,不是引用地址指向是相同的吗
改变克隆对象的姓名,发现只有一个改变了,另一个没有改变
原型 :张三
克隆 :班主任
这里说一个比较重要的知识点,那就是String的不变性的特性,String、Integer等包装类都是不可变的,当需要修改不可变值时,需要在内存里生成一个新的对象来存放新的值,然后将原来的引用指向新的地址 ,所以我们这里修改了克隆的name值,克隆的name值指向内存中新的name对象,但是我们并没有改变原型的name指向,所以原型的name值还是指向的原来的地址。所以没有变。
3.浅拷贝后可以看到引用地址都相同,所以不管改变原型还是克隆的属性,都会一起改变,说明浅拷贝后,他们指向了同一个属性引用。
通过这里可以看到指向了同一个引用 证明了原型和克隆用的是同一个引用地址
孩子原型的克隆内存地址javaspring.prant.cloneable.Student$XStudent@66a29884
克隆孩子的内存地址javaspring.prant.cloneable.Student$XStudent@66a29884------------------------------------------------------------------------------------------------------------------
改变他们相同引用的值 发现改变任意一个 都会一起更改
原型孩子 :bbb
克隆孩子 :bbb
个人理解:相对来说浅拷贝是不太安全的,因为两个对象持有相同的引用地址,不管修改哪个值都会影响另一个对象,这样的办法是不太可取的,解决办法就是实现深拷贝,可以完全拷贝一个新的对象。
深拷贝(Deep Copy)
深拷贝,顾名思义,就是不管原型是什么,都会复制一个全新的对象,拷贝对象的修改,不会影响原型对象,也就是都有自己的空间。
具体实现深拷贝方式有两种:
第一种实现:重写clone()方法,适用于类中属性引用少的情况下,一般不推荐使用,对象的引用属性也要实现cloneable这个接口,需要将Student的方法修改一下
演示代码:
public class Student implements Cloneable
{
private int id;
private String name;
private XStudent xStudent;
public Student(int id, String name, XStudent xStudent)
{
this.id = id;
this.name = name;
this.xStudent = xStudent;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public XStudent getxStudent() {
return xStudent;
}
public void setxStudent(XStudent xStudent) {
this.xStudent = xStudent;
}
//重写父类方法,实现深克隆
@Override
protected Object clone() throws CloneNotSupportedException
{
Student student= (Student) super.clone();
student.xStudent= (XStudent) xStudent.clone();
return student;
}
static class XStudent implements Cloneable
{
private String name;
private int age;
public XStudent(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
//引用clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
测试main()方法用上面哪个就可以了 这里看一下运行结果:
原型对象的内存地址 :javaspring.prant.cloneable.Student@70177ecd
克隆对象的内存地址 :javaspring.prant.cloneable.Student@1e80bfe8
原型孩子的克隆内存地址javaspring.prant.cloneable.Student$XStudent@66a29884
克隆孩子的内存地址javaspring.prant.cloneable.Student$XStudent@4769b07b
原型 :张三
克隆 :班主任
原型孩子 :李五
克隆孩子 :bbb
通过上面可以看到,深拷贝后,产生了一个新的对象,并且拷贝了一份引用,所以修改一个的值另一个不会修改,因为他们指向的不是一个引用。
第二种使用实现序列化接口(Serializable)来实现
public class Student implements Serializable
{
private int id;
private String name;
private XStudent xStudent;
public Student(int id, String name, XStudent xStudent)
{
this.id = id;
this.name = name;
this.xStudent = xStudent;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public XStudent getxStudent() {
return xStudent;
}
public void setxStudent(XStudent xStudent) {
this.xStudent = xStudent;
}
static class XStudent implements Serializable
{
private String name;
private int age;
public XStudent(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
}
测试类看结果
public class Run
{
ObjectInputStream objectInputStream=null;
ObjectOutputStream objectOutputStream=null;
public static void main(String[] args)
{
Student.XStudent xStudent=new Student.XStudent("mmj",2);
Student student=new Student(1,"mmd",xStudent);
System.out.println("内存地址");
Student student1=new Run().getStudent(student,xStudent);
System.out.println(student);
System.out.println(student1);
System.out.println("------------------------------------------");
System.out.println("引用地址");
Student.XStudent xStudent1=student.getxStudent();
Student.XStudent xStudent2=student1.getxStudent();
System.out.println(xStudent1);
System.out.println(xStudent2);
System.out.println("------------------------------------------");
System.out.println("修改看看会不会影响另一个对象");
student.setName("你虚伪");
System.out.println(student.getName());
System.out.println(student1.getName());
}
private Student getStudent(Student student, Student.XStudent xStudent) {
xStudent=new Student.XStudent("mmj",2);
student=new Student(1,"mmd",xStudent);
Student student1=null;
ByteArrayOutputStream arrayOutputStream=new ByteArrayOutputStream();
try {
objectOutputStream=new ObjectOutputStream(arrayOutputStream);
//序列化传递对象
objectOutputStream.writeObject(student);
objectOutputStream.flush();
ByteArrayInputStream arrayInputStream=new ByteArrayInputStream(arrayOutputStream.toByteArray());
objectInputStream=new ObjectInputStream(arrayInputStream);
student1=(Student) objectInputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
if (objectOutputStream!=null){
}
if (objectInputStream!=null){
}
}
return student1;
}
}
这里看到的结果和上面的一样都实现了深拷贝。
内存地址
javaspring.prant.cloneable.Student@1ed6993a
javaspring.prant.cloneable.Student@7e32c033
------------------------------------------
引用地址
javaspring.prant.cloneable.Student$XStudent@7ab2bfe1
javaspring.prant.cloneable.Student$XStudent@497470ed
------------------------------------------
修改看看会不会影响另一个对象
你虚伪
mmd
当然在平时的工作中,上面的方式都显的特别繁琐,所以我们最好用工具去实现,比如apache的lang3的工具类。
第一步导入apache的lang3的jar包,也就是我们说的依赖。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
public class Main
{
public static void main(String[] args)
{
Student.XStudent xStudent=new Student.XStudent("mmj",2);
Student student=new Student(1,"mmd",xStudent);
//序列化的具体实现
byte[] bytes= SerializationUtils.serialize(student);
Student student1=SerializationUtils.deserialize(bytes);
System.out.println("内存地址");
System.out.println(student);
System.out.println(student1);
System.out.println("------------------------------------------");
System.out.println("引用地址");
Student.XStudent xStudent1=student.getxStudent();
Student.XStudent xStudent2=student1.getxStudent();
System.out.println(xStudent1);
System.out.println(xStudent2);
System.out.println("------------------------------------------");
System.out.println("修改看看会不会影响另一个对象");
student.setName("你虚伪");
System.out.println(student.getName());
System.out.println(student1.getName());
}
}
运行结果都是和上面哪个实现序列化接口的一样。这样看起来是不是非常的简便。
总结:
不同的工作场景,运用的拷贝方式不同,只要我们了解了原理,就能权衡利弊的去选择哪种方式更好,没有更好,只有合适。
1.如果对象的属性都是基本类型,可以用到浅拷贝。
2.如果对象有引用类型属性,那就要根据需求决定用深拷贝还是浅拷贝了。
3.如果对象的引用都不会被改变,那就没必要用深拷贝了。如果对象引用经常变,那就用深引用。
没有一尘不变的规则,一切都取决于需求。浅拷贝和深拷贝都有利弊。