- 你好,我是刘超。
- 要实现⼀个原型类,需要具备三个条件:
- 深拷⻉和浅拷⻉
- 适⽤场景
27讲原型模式与享元模式:提升系统性能的利器
你好,我是刘超。
原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进⾏调优;后者是⽤减少创建实例的⽅式,来调优系统性能。这么看,你会不会觉得两个模式有点相互⽭盾呢?
其实不然,它们的使⽤是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值⼀个对象,此时我们就可以采⽤原型模式来优化对象的创建过程;⽽在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了。
今天我们就来看看这两种模式的适⽤场景,了解了这些你就可以更⾼效地使⽤它们提升系统性能了。
原型模式
我们先来了解下原型模式的实现。原型模式是通过给出⼀个原型对象来指明所创建的对象的类型,然后使⽤⾃身实现的克隆接
⼝来复制这个原型对象,该模式就是⽤这种⽅式来创建出更多同类型的对象。
使⽤这种⽅式创建新的对象的话,就⽆需再通过new实例化来创建对象了。这是因为Object类的clone⽅法是⼀个本地⽅法,它可以直接操作内存中的⼆进制流,所以性能相对new实例化来说,更佳。
实现原型模式
我们现在通过⼀个简单的例⼦来实现⼀个原型模式:
//实现Cloneable 接⼝的原型抽象类Prototype class Prototype implements Cloneable {
//重写clone⽅法
public Prototype clone(){ Prototype prototype = null; try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){ e.printStackTrace();
}
return prototype;
}
}
//实现原型类
class ConcretePrototype extends Prototype{ public void show(){
System.out.println(“原型模式实现类”);
}
}
public class Client {
public static void main(String[] args){ ConcretePrototype cp = new ConcretePrototype(); for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)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的值也就跟着被修改了。
我们可以通过⼀个简单的例⼦来看看普通的对象复制问题:
class Student {
private String name;
public String getName() { return name;
}
public void setName(String name) { this.name= name;
}
}
public class Test {
public static void main(String args[]) { Student stu1 = new Student(); stu1.setName(“test1”);
Student stu2 = stu1; stu1.setName(“test2”);
System.out.println(“学⽣1:” + stu1.getName());
System.out.println(“学⽣2:” + stu2.getName());
}
}
如果是复制对象,此时打印的⽇志应该为:
然⽽,实际上是:
通过clone⽅法复制的对象才是真正的对象复制,clone⽅法赋值的对象完全是⼀个独⽴的对象。刚刚讲过了,Object类的clone
⽅法是⼀个本地⽅法,它直接操作内存中的⼆进制流,特别是复制⼤对象时,性能的差别⾮常明显。我们可以⽤ clone ⽅法再实现⼀遍以上例⼦。
//学⽣类实现Cloneable接⼝
class Student implements Cloneable{ private String name; //姓名
public String getName() { return name;
}
public void setName(String name) { this.name= name;
}
//重写clone⽅法
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
深拷⻉和浅拷⻉
在调⽤super.clone()⽅法之后,⾸先会检查当前对象所属的类是否⽀持clone,也就是看该类是否实现了Cloneable接⼝。
如果⽀持,则创建当前对象所属类的⼀个新对象,并对该对象进⾏初始化,使得新对象的成员变量的值与当前对象的成员变量的值⼀模⼀样,但对于其它对象的引⽤以及List等类型的成员属性,则只能复制这些对象的引⽤了。所以简单调⽤
super.clone()这种克隆对象⽅式,就是⼀种浅拷⻉。
所以,当我们在使⽤clone()⽅法实现对象的克隆时,就需要注意浅拷⻉带来的问题。我们再通过⼀个例⼦来看看浅拷⻉。
//定义学⽣类
class Student implements Cloneable{ private String name; //学⽣姓名private Teacher teacher; //定义⽼师类
public String getName() { return name;
}
public void setName(String name) { this.name = name;
}
public Teacher getTeacher() { return teacher;
}
public void setName(Teacher teacher) { this.teacher = teacher;
}
//重写克隆⽅法
public Student clone() { Student student = null; try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) { e.printStackTrace();
}
return student;
}
}
//定义⽼师类
class Teacher implements Cloneable{
private String name; //⽼师姓名
public String getName() { return name;
}
public void setName(String name) { this.name= name;
}
//重写克隆⽅法,堆⽼师类进⾏克隆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;
}
适⽤场景
前⾯我详讲了原型模式的实现原理,那到底什么时候我们要⽤它呢?
在⼀些重复创建对象的场景下,我们就可以使⽤原型模式来提⾼对象的创建性能。例如,我在开头提到的,循环体内创建对象时,我们就可以考虑⽤clone的⽅式来实现。
例如:
我们可以优化为:
Student stu = new Student(); for(int i=0; i
…
}
除此之外,原型模式在开源框架中的应⽤也⾮常⼴泛。例如Spring中,@Service默认都是单例的。⽤了私有全局变量,若不想影响下次请求,就需要⽤到原型模式,我们可以通过以下注解来实现,@Scope(“prototype”)。
享元模式
享元模式是运⽤共享技术有效地最⼤限度地复⽤细粒度对象的⼀种模式。该模式中,以对象的信息状态划分,可以分为内部数
据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运⾏⽽改变;外部数据则是在不同运⾏时被标
记了不同的值。
享元模式⼀般可以分为三个⻆⾊,分别为 Flyweight(抽象享元类)、ConcreteFlyweight(具体享元类)和
FlyweightFactory(享元⼯⼚类)。抽象享元类通常是⼀个接⼝或抽象类,向外界提供享元对象的内部数据或外部数据;具体享元类是指具体实现内部数据共享的类;享元⼯⼚类则是主要⽤于创建和管理享元对象的⼯⼚类。
实现享元模式
我们还是通过⼀个简单的例⼦来实现⼀个享元模式:
//抽象享元类
interface Flyweight {
//对外状态对象
void operation(String name);
//对内对象
String getType();
}
//具体享元类
class ConcreteFlyweight implements Flyweight { private String type;
public ConcreteFlyweight(String type) { this.type = type;
}
@Override
public void operation(String name) {
System.out.printf(“[类型(内在状态)] - [%s] - [名字(外在状态)] - [%s]\n”, type, name);
}
@Override
public String getType() { return type;
}
}
//享元⼯⼚类
class FlyweightFactory {
private static final Map
public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {//如果在享元池中存在对象,则直接获取return FLYWEIGHT_MAP.get(type);
} else {//在响应池不存在,则新创建对象,并放⼊到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type); FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}
public class Client {
public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight(“a”); Flyweight fw1 = FlyweightFactory.getFlyweight(“b”); Flyweight fw2 = FlyweightFactory.getFlyweight(“a”); Flyweight fw3 = FlyweightFactory.getFlyweight(“b”); fw1.operation(“abc”);
System.out.printf(“[结果(对象对⽐)] - [%s]\n”, fw0 == fw2);
System.out.printf(“[结果(内在状态)] - [%s]\n”, fw1.getType());
}
}
输出结果:
[类型(内在状态)] - [b] - [名字(外在状态)] - [abc]
[结果(对象对⽐)] - [true]
[结果(内在状态)] - [b]
观察以上代码运⾏结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,⽽是共⽤享元池中内部数据
⼀致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占⽤的内存空间。
适⽤场景
享元模式在实际开发中的应⽤也⾮常⼴泛。例如Java的String字符串,在⼀些字符串常量中,会共享常量池中字符串对象,从
⽽减少重复创建相同值对象,占⽤内存空间。代码如下:
String s1 = “hello”; String s2 = “hello”;
System.out.println(s1==s2);//true
还有,在⽇常开发中的应⽤。例如,线程池就是享元模式的⼀种实现;将商品存储在应⽤服务的缓存中,那么每当⽤户获取商
品信息时,则不需要每次都从redis缓存或者数据库中获取商品信息,并在内存中重复创建商品信息了。
总结
通过以上讲解,相信你对原型模式和享元模式已经有了更清楚的了解了。两种模式⽆论是在开源框架,还是在实际开发中,应
⽤都⼗分⼴泛。
在不得已需要重复创建⼤量同⼀对象时,我们可以使⽤原型模式,通过clone⽅法复制对象,这种⽅式⽐⽤new和序列化创建 对象的效率要⾼;在创建对象时,如果我们可以共⽤对象的内部数据,那么通过享元模式共享相同的内部数据的对象,就可以减少对象的创建,实现系统调优。
思考题
上⼀讲的单例模式和这⼀讲的享元模式都是为了避免重复创建对象,你知道这两者的区别在哪⼉吗?
期待在留⾔区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。
精选留⾔
⽊⽊匠
单例模式是针对某个类的单例,享元模式可以针对⼀个类的不同表现形式的单例,享元模式是单例模式的超集。
2019-07-25 08:16
作者回复
⾔简意赅!
2019-07-27 12:20
QQ怪
享元模式可以再次创建对象 也可以取缓存对象
单例模式则是严格控制单个进程中只有⼀个实例对象
享元模式可以通过⾃⼰实现对外部的单例 也可以在需要的使⽤创建更多的对象单例模式是⾃身控制 需要增加不属于该对象本身的逻辑
2019-07-25 20:25
作者回复
理解很透彻,点赞
2019-07-26 09:56
知⾏合⼀
new⼀个对象和clone⼀个对象,性能差在哪⾥呢?⽂中提到直接从内存复制⼆进制这⾥不是很理解
2019-07-26 08:42
作者回复
⼀个对象通过new创建的过程为:
1、在内存中开辟⼀块空间;
2、在开辟的内存空间中创建对象;
3、调⽤对象的构造函数进⾏初始化对象。
⽽⼀个对象通过clone创建的过程为:
1、根据原对象内存⼤⼩开辟⼀块内存空间;
2、复制已有对象,克隆对象中所有属性值。
相对new来说,clone少了调⽤构造函数。如果构造函数中存在⼤量属性初始化或⼤对象,则使⽤clone的复制对象的⽅式性能会好⼀些。
2019-07-26 14:31
Aaron
东⽅奇骥
⽼师,请教⼀下,⽂中说的,@service默认是单例模式,若不想影响下次请求,就要使⽤原型模式。能举个例⼦吗,什么时候会影响下次请求,不是很理解,因为我的项⽬⾥基本都是单例模式
2019-07-27 17:28
Aaron
⽼师请教你个问题,线上短信业务被轰炸,流量费倍增……求推荐个解决思路,监测发现是爬⾍程序
2019-07-26 09:01
作者回复
建议加⼀个图⽚验证码
2019-07-27 12:17
Liam
⽼师好,⽂中举例Spring的prototype貌似不是原型模式的实现吧,每次spring都是通过反射创建的对象,并没有通过clone的⽅ 式吧
2019-07-26 08:05
⻔窗⼩⼆
通过⽼师这次的讲解算是彻底明⽩了单例模式与享元模式的区别,享元模式可以理解为⼀组单例模式
作者回复
对的
2019-07-26 09:55
程序员⼈⽣
单例模式的运⽤场景⼀般是⼀个系统中的全局事物,⽐如数据库连接池,多线程连接池等
享元模式的运⽤场景则是是业务流程中,需要频繁获取元数据的的情况,⽐如⽼师说的⽤户获取商品的情况。
2019-07-25 12:08
Jxin
实现⼀个公共⽗类,实现原型模式,并反射完成深拷⻉。对需要⼤量创建新对象的类继承该⽗类。⽼师这样做⾏不?反射有开销,继承这种结构也不好(但组合实现感觉不直观)。不确定这样抽象后是否利⼤于弊。毕竟如果反射开销冲掉了clone带来的性能优化,还不如直接new
2019-07-25 09:48
我已经设置了昵称
享元模式和策略模式感觉有点像啊,根据某个值,去上下⽂容器中取对应的handler类处理
2019-07-25 09:07
我已经设置了昵称
在a=b的地⽅的代码是否有问题,第⼆个stu1.setName(“test2”);应该改为stu2.setName(“test2”)
2019-07-25 08:56
-W.LI-
⽼师好,具体啥时候⽤原型模式啊?循环有很多,中不能每个DTO都搞个原型类吧能透露下评价原则么?谢谢⽼师pojo,dto,vo, 再加⼀个原型类爆炸。
2019-07-25 08:48
-W.LI-
// 抽象享元类
interface Flyweight {
// 对外状态对象
void operation(String name);
// 对内对象
String getType();
}
⽼师好!享员⼯⼚为啥不⽤operation的⼊参name做key?
2019-07-25 08:44
作者回复
因为name是对外状态的⼀个对象,不存在共享。
2019-07-27 12:19
全有
享元模式的给⼯⼚类,是⽤HashMap 来存储共享对象,在多线程下并不安全,同时也没有加锁判定,依然会存在创建个对象
,只是会覆盖掉
2019-07-25 08:19
a、
我觉得单例模式和享元模式的区别,享元模式可以看成是⼀组单例模式。
2019-07-25 01:21