在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。
目录
一、原型模式定义
原型模式是一种创建型设计模式
,原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型
,通过复制该原型对象来创建一个和原型相同或相似的新对象
。在这里,原型实例就是要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。在生活中复制的例子非常多,这里不一一列举了。
实例是什么意思?
对于一些初学者Java的来讲有的其实对实例这个词很迷茫,以下代码为例:b就是A类的一个实例
,为什么说是实例,因为他是真实存在的一个对象,我们这个时候可以调用他的方法,可以给他的属性存值等等。有的可能会说叫实例对象
,其实是一个意思。
A b = new A();
原型是什么意思?
这个词说实话刚开始我也没理解,以下代码为例:通过b实例克隆了一个c出来,那么b就是c的原型实例
。
A b = new A();
A c = b.clone();
二、什么场景会用到
这里我直接拿场景来举例,方便理解一点。
假如有个a对象,但是a对象结构比较复杂,他有很多属性,比如有list属性,还有map等等一系列属性,这时候我想有一个跟他一模一样的对象,需要怎么做?
A a = new A();
可能有的人该说了,什么场景下会要这种无理的要求,事实上有时候真的会遇到,只不过很少。有的人会想到用以下这种方式,其实这相当于声明了个b变量,去指向了上一个对象,说白了,就是a和b就是一个对象,你修改b,a也会随之改变。那他还有存在的意义吗,我们要的是跟他一模一样的对象,并且修改复制出来的对象,原来的对象也不会发生变化。
A a = new A();
A b = a ;
这时候就有人该说了,我直接重新new一个对象就好了呀,这里有一点要注意,你new出来的是新对象,我们要的是和之前的对象属性都要一致。然后又有人该说了,那我就重新给新对象set一遍属性值。
以上确实是可以做到,但是属性值要是很多呢,效率慢,而且代码很臃肿,这时候原型模式就诞生了。原型模式是通过将a对象使用二进制流的方式,直接进行克隆,生成新的对象。
三、原型模式实现思路
Java中Object类是所有类的根类,Object类提供了一个clone()方法,该方法可以
将一个Java对象复制一份,但是需要实现clone的Java类必须要实现一个接口Cloneable,该接口表示该类能够复制且具有复制的能力 => 原型模式
四、浅克隆和深克隆
1、浅克隆和深克隆的区别
浅克隆是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
深克隆不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:
2、浅克隆实现方式
这里有一点要注意一下,必须要实现Cloneable接口,才可以使用clone方法,不然直接报错。
public class Student implements Cloneable {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
// get、set省略
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
/**
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student(20, "张三");
Student student2 = (Student) student1.clone();
System.out.println(student2.toString());
student2.setAge(22);// 注意修改student2的age值 但是没有影响 student1的值
student2.setName("李四");
System.out.println("student1:" + student1.getName() + "-->" + student1.getAge());
System.out.println("student2:" + student2.getName() + "-->" + student2.getAge());
}
}
输出结果:
从以上例子当中,似乎一切正常,有人该说了,string不是引用类型吗,那我修改了student2,按正常来说浅克隆是不克隆引用类型的对象的,只是将变量指向了原有的对象,那他也应该跟着改变才对呀。简单提一嘴。
string是特殊的引用类型,正常的引用类型当创建一个对象a,然后又创建了一个变量,指向了a,这时候修改这个变量,而a也会随之变化,这才是真正的引用类型。
下面示例当中,我声明了s1,指向了s变量引用的String类型对象,修改了s1,但是s引用的对象并未发生变化。原因:因为string他实际字符串是存在常量池的(底层是一个final的char数组)。而并非堆当中,堆只是一个指向常量池的一个指针。所以我们修改string的时候,本质上常量池是没有发生变化的,只是将指针指向了常量池的另一个地方。
String是正宗的引用类型,但是,一定条件下,String会表现出一定的值特性。
String s = new String("1");
String s1 = s;
System.out.println(s == s1);
s1 = "2";
System.out.println(s);
System.out.println(s1);
输出结果:
浅克隆引用类型问题:
class Teacher implements Cloneable {
private String name;
private Student student;
// get、set省略
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setAge(20);
s1.setName("张三");
Teacher teacher1 = new Teacher();
teacher1.setName("小赵老师");
teacher1.setStudent(s1);
//为什么会出现以下结果, Teacher中的clone方法
Teacher teacher2 = (Teacher)teacher1.clone();
Student s2 = teacher2.getStudent();
s2.setName("李四");
s2.setAge(30);
System.out.println("teacher1:"+teacher1);
System.out.println("teacher2:"+teacher2);
}
}
运行结果:
teacher1:Teacher [name=小赵老师, student=Student [age=30, name=李四]]
teacher2:Teacher [name=小赵老师, student=Student [age=30, name=李四]
3、深克隆实现方式
要克隆的类和类中所有非基本数据类型的属性对应的类
在clone()方法注意点,需要将引用类型的属性对象clone,否则就是浅克隆了。
深拷贝实现方式1:重写clone方法来实现深拷贝
深拷贝实现方式2:通过对象序列化实现深拷贝(这种的一般我们不会手写,在很多工具包会做封装,我们只需要将对象实现序列化接口,然后调用方法即可,在工具类当中会提到)
class Teacher implements Cloneable {
private String name;
private Student student;
// get、set省略
@Override
public String toString() {
return "Teacher [name=" + name + ", student=" + student + "]";
}
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
//注意以下代码
Teacher teacher = (Teacher)super.clone();
teacher.setStudent((Student)teacher.getStudent().clone());
return teacher;
}
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setAge(20);
s1.setName("张三");
Teacher teacher1 = new Teacher();
teacher1.setName("小赵老师");
teacher1.setStudent(s1);
Teacher teacher2 = (Teacher)teacher1.clone();
teacher2.setName("小明老师");
Student s2 = teacher2.getStudent();
s2.setName("李四");
s2.setAge(30);
System.out.println("teacher1:"+teacher1);
System.out.println("teacher2:"+teacher2);
}
}
运行结果:
teacher1:Teacher [name=小赵老师, student=Student [age=20, name=张三]]
teacher2:Teacher [name=小明老师, student=Student [age=30, name=李四]]
五、原型模式优缺点
优点:
- Java 自带的原型模式
基于内存二进制流的复制
(源码当中clone是一个native方法,也就是clone复制实例,是内存中直接复制了数据块,不会调用构造方法,少了对象的初始化这些操作
),在性能上比直接 new 一个对象更加优良。 - 可以使用
深克隆方式保存对象的状态
,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类都配置一个 clone 方法(想要使用clone方法,需要实现接口的)
- clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
- 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
六、原型模式在Spring框架中源码分析
1、socop属性
spring当中有个socop属性就是设置bean 的作用范围的生命周期,默认scope是单例的。
单例对象:scope=“singleton”
多例对象:scope=“prototype” (prototype就是原型的意思)
Scope有两种配置方式,一种基于xml的,一种基于注解的。
<bean id="" class="impl.AccountServiceImpl" scope="prototype"></bean>
@RestController
@RequestMapping(value = "/client")
@Scope("prototype")
public class ClientController {
}
2、代码示例
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
这个xml相当于是将SpringScopTest存入容器。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="springScopTest" class="com.gzl.cn.SpringScopTest" scope="prototype">
</bean>
</beans>
public class SpringScopTest {
public SpringScopTest() {
System.out.println("无参构造器执行");
}
}
public class SpringTest {
public static void main(String[] args) throws NoSuchMethodException {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
Object springScopTest = ac.getBean("springScopTest");
System.out.println(springScopTest);
Object springScopTest1 = ac.getBean("springScopTest");
System.out.println(springScopTest1);
}
}
运行结果:
从运行结果可以看出,实际上Spring的原型模式,就是每获取一次他都会通过构造器创建一次。
3、源码分析
clazz.getDeclaredConstructor()
方法返回一个Constructor对象,它反映此Class对象所表示的类或接口指定的构造函数。从这里可以看出他实际上是通过无参构造进行创建的新对象。
4、源码总结
这里我拿这块代码做了个试验,下面代码代表将user类注入容器,并且scope设置为了原型,然后我从容器获取了两次,然后他这段代码执行了两次,也就意味着,spring原型实际上不是我们上面所说的浅克隆和深克隆进行 实现的,只是将创建对象的方法给重新构建 了一遍。
@Bean
@Scope(scopeName = "prototype")
private User user(){
User user = new User();
user.setAge(1);
user.setName("张三");
user.setUser(user);
return user;
}
七、拷贝
克隆:几乎等于一个印章的作用.可以随便去按下印章
拷贝(也可以叫复制):拷贝有点类似复印,相当于拿身份证去复印,前提得有纸和身份证才能复印,拷贝就是将要拷贝的对象,拷贝到一个现有的对象当中。
我看网上的一些博客资料等等,我看是不区分这两种的区别的,当作了一个东西来看待,我个人感觉克隆和拷贝还是有点区别的。
1、什么是拷贝
Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a; B.b=A.b;
2、拷贝实现方式
这里我直接用的springboot当中的BeanUtils工具类。
原理:通过getReadMethod 与 getWriteMethod获取类中定义的get,set方法,然后进行执行copy。
import org.springframework.beans.BeanUtils;
// old代表要拷贝的对象,new代表新创建的对象
BeanUtils.copyProperties(old, new);
八、常用工具类
1、hutool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
实现浅克隆的时候,需要实现Cloneable接口,然后重写clone方法,而hutool工具包提供了泛型克隆类,CloneSupport这个类帮我们实现了上面的clone方法,因此只要继承此类,不用写任何代码即可使用clone()方法:
/**
* 狗狗类,用于继承CloneSupport类
* @author Looly
*
*/
private static class Dog extends CloneSupport<Dog>{
private String name = "wangwang";
private int age = 3;
}
深克隆实现方式:
前提是对象必须实现Serializable接口。
ObjectUtil.cloneByStream(obj)
2、commons-lang3
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
import org.apache.commons.lang3.SerializationUtils;
User copyUser = (User)SerializationUtils.clone(user);
九、总结
相比较而言,克隆实际开发当中用的较少,原因就是因为他需要改动实体类,实现接口重写方法等等操作,而浅拷贝
往往是开发当中经常会用到的。
尤其是框架当中使用的是JPA(对象映射框架)的,因为JPA从数据库当中查出来对象,有时候我们并不想修改数据库当中的这个对象,只是想着拿查出来的这个对象,作为实体类向别的方法传递等等,假如修改实体类,数据库的值也会跟着变。
一般就是先从数据库查出来,然后new一个对象,将查出来的对象属性拷贝到新对象当中。