定义
这节中我们将介绍一种新的结构型模式——享元模式 (Flyweight Pattern),Flyweight 模式是以共享的模式来支持大量细粒度对象的复用。听起来可能有点绕,Java 中的 String 就是享元模式的一个应用:
String a = "abc";
String b = "abc";
System.out.println(a == b); // true
上述例子中,分别创建两个字符串对象的时候,a、b 其实都指向了常量池中的某个字符串“abc”,这种对象创建的模式,就避免了大量对象创建时非必要的资源消耗,享元模式的“享”就有一物被众人所共享的意思,所以享元模式也是池技术的重要实现方式,其定义如下:使用共享对象有效地支持大量的细粒度的对象。
使用实例
下面我们用一个最简单的考试报名的例子进行说明,假设我们有 2 个科目,有 3 位考生分别进行报考,我们一般会定义考试实体 ExamInfo,如果不使用模式的话,可以想象,每次有考生参与一场科目考试的话,我们就会实例化一个 ExamInfo,总共我们要实例化 6 个这样的实体,倘若使用享元模式,我们就只需要实例化 2 个这样的实体,然后通过内部状态的 set 方法进行不同对象的赋值操作,节省了不少的内存,很神奇吧?
先来看看我们的 ExamInfo 如何?
public class ExamInfo {
// 内部状态,用于在各个对象之间共享,不随环境改变而改变,存储在享元对象内部,往往作为对象的动态附加信息存在
private String user; // 考生
// 外部状态,随环境改变而改变,属于不可共享的状态,是对象得以依赖的一个标记
private String subject; // 考试科目
public ExamInfo(String subject) {
this.subject = subject;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
@Override
public String toString() {
return "ExamInfo{" +
"user='" + user + '\'' +
", subject='" + subject + '\'' +
'}';
}
}
在考试信息里面,我们将考试科目作为考试信息的唯一标识,以此来实现对象复用。
然后是我们的享元工厂,也就是我们的池,该池中留有细粒度对象的引用实例,具体代码如下:
public class ExamInfoFactory {
// 对象池,用来支持细粒度对象的复用
private static HashMap<String, ExamInfo> pool = new HashMap<>();
public static ExamInfo getExamInfo(String subject) {
// 设置返回的对象
ExamInfo examInfo = null;
if (!pool.containsKey(subject)) {
System.out.println("建立对象,并放到池中..." + subject);
examInfo = new ExamInfo(subject);
pool.put(subject, examInfo);
} else {
examInfo = pool.get(subject);
System.out.println("直接从池中获取..." + subject);
}
return examInfo;
}
}
这里我们使用一个 map 对象来实现池的功能,通过科目我们可以获取池中的某个考试信息实例,如果存在则直接从池中获取返回,如果不存在,我们会手动 new 一个考试信息,然后再将其放入池中,之后再返回。
接下来是我们的测试类,也就是 main 函数:
public class Main {
public static void main(String[] args) {
// 假设有2个科目,初始化一遍池子
for (int i = 0; i < 2; i++) {
String subject = "科目" + i;
ExamInfoFactory.getExamInfo(subject);
}
// 假设3个考生考试
for(int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
ExamInfo examInfo = ExamInfoFactory.getExamInfo("科目" + j);
examInfo.setUser("考生" + i);
System.out.println(examInfo);
}
}
}
}
测试输出如下:
建立对象,并放到池中...科目0
建立对象,并放到池中...科目1
直接从池中获取...科目0
ExamInfo{user='考生0', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生0', subject='科目1'}
直接从池中获取...科目0
ExamInfo{user='考生1', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生1', subject='科目1'}
直接从池中获取...科目0
ExamInfo{user='考生2', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生2', subject='科目1'}
从上面的信息中我们可以发现,实际上初始化 ExamInfo 实体的操作我们只进行了 2 次,也就说内存中保存的对象引用只有 2 份,相比预不使用享元模式需要进行 6 次实例化的方案是不是一下省了部分内存,设想一下,倘若有 100000 个考生那又节省了多少内存?
组成角色
享元模式包含的角色列举如下:
- 抽象享元角色(Flyweight):一般是一个具体的抽象类,同时定义了对象的外部状态和内部状态的接口或实现;
- 具体享元角色(ConcreteFlyweight):具体的一个产品类,实现了抽象享元角色中定义的接口,该角色需要注意的是内部状态的处理应该与环境无关;
- 享元工厂(FlyweightFactory):该角色指责一般比较清晰,就是一个池工厂,提供池对象和获取池中对象的方法
享元模式代码实现
享元模式的几个角色上面已经介绍了,该角色对应的通用代码如下,首先是抽象享元角色:
public abstract class Flyweight {
// 内部状态
private String intrinsic;
// 外部状态
protected final String extrinsic;
public Flyweight(String extrinsic) {
this.extrinsic = extrinsic;
}
// 定义的业务操作
public abstract void operate();
// 内部状态的getter、setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}
接下来是具体的享元角色:
public class ConcreteFlyweight extends Flyweight{
// 接受外部状态
public ConcreteFlyweight(String extrinsic) {
super(extrinsic);
}
// 根据外部状态进行一些逻辑处理
@Override
public void operate() {
// 具体的业务逻辑
}
}
最后是享元工厂:
public class FlyweightFactory {
// 池容器
private static HashMap<String, Flyweight> pool = new HashMap<>();
// 获取池中对象
public static Flyweight getFlyweight(String extrinsic) {
// 需要返回的对象
Flyweight flyweight = null;
if (!pool.containsKey(extrinsic)) {
// 根据外部状态创建享元对象
flyweight = new ConcreteFlyweight(extrinsic);
// 再放入池中
pool.put(extrinsic, flyweight);
} else {
// 池中有对象,则直接返回
flyweight = pool.get(extrinsic);
}
return flyweight;
}
}
总结
本节中,我们讲了下什么是享元模式,以及为什么要使用享元模式,下面我们归纳下本节的知识点,包括享元模式的应用场景,总结下如下: