享元模式
案例
张三和李四刚刚考完期中考试的语文和数学,但不是很理想。老师在课堂的讲的感觉还不是很懂,所以想找老师要答案仔细看看解题过程。接下来就用程序来模拟这一过程,假设考了语文和数学两个科目。
1.首先定义两个试卷类:
语文试卷类:
/**
* 语文试卷答案类
*/
public class ChineseTestAnswer {
// 试卷答案内容
private String answer;
public ChineseTestAnswer() {
System.out.println("老师从试卷库中取出语文试卷答案带进教室");
this.answer = "答案:A、B、C...";
}
public String showAnswer() {
return this.answer;
}
}
数学试卷类:
/**
* 数学书卷答案类
*/
public class MathTestAnswer {
// 试卷答案内容
private String answer;
public MathTestAnswer() {
System.out.println("老师从试卷库中取出数学试卷答案带进教室");
this.answer = "答案:C、B、A...";;
}
public String showAnswer() {
return this.answer;
}
}
2.然后是学生类:
/**
* 学生类
*/
public class Student {
private String name;
private ChineseTestAnswer chineseTestAnswer;
private MathTestAnswer mathTestAnswer;
public Student(String name) {
this.name = name;
}
public void showChineseAnswer() {
System.out.println("学生" + this.name + "查看语文试卷答案[" + chineseTestAnswer.showAnswer() + "]");
}
public void showMathTestAnswer() {
System.out.println("学生" + this.name + "查看语文试卷答案[" + mathTestAnswer.showAnswer() + "]");
mathTestAnswer.showAnswer();
}
public void setChineseTestAnswer(ChineseTestAnswer chineseTestAnswer) {
System.out.println("老师把语文试卷答案发给学生:" + this.name);
this.chineseTestAnswer = chineseTestAnswer;
}
public void setMathTestAnswer(MathTestAnswer mathTestAnswer) {
System.out.println("老师把数学试卷答案发给学生:" + this.name);
this.mathTestAnswer = mathTestAnswer;
}
}
3.测试类:
public class Main {
public static void main(String[] args) {
Student zs = new Student("张三");
zs.setChineseTestAnswer(new ChineseTestAnswer());
zs.showChineseAnswer();
zs.setMathTestAnswer(new MathTestAnswer());
zs.showMathTestAnswer();
System.out.println("-----------------------------------------");
Student ls = new Student("李四");
ls.setChineseTestAnswer(new ChineseTestAnswer());
ls.showChineseAnswer();
ls.setMathTestAnswer(new MathTestAnswer());
ls.showMathTestAnswer();
}
}
4.测试结果:
老师从试卷库中取出语文试卷答案带进教室
老师把语文试卷答案发给学生:张三
学生张三查看语文试卷答案[答案:A、B、C...]
老师从试卷库中取出数学试卷答案带进教室
老师把数学试卷答案发给学生:张三
学生张三查看语文试卷答案[答案:C、B、A...]
-----------------------------------------
老师从试卷库中取出语文试卷答案带进教室
老师把语文试卷答案发给学生:李四
学生李四查看语文试卷答案[答案:A、B、C...]
老师从试卷库中取出数学试卷答案带进教室
老师把数学试卷答案发给学生:李四
学生李四查看语文试卷答案[答案:C、B、A...]
从这一过程可以看到,老师给每个人的每个科目都拿了考试答案,这样张三和李四就可以愉快的参考答案仔细解决自身的问题所在了。但是作为一个以节约为荣,一浪费为耻的张三觉得,老师给每个人的各个科目都发答案浪费了纸张。他觉得只用给每个科目一份答案就好了,一个人再看语文答案的时候,另一个人可以看数学答案。这样试卷的答案就只用有一份就可以了,而且其他同学想看,也不用在浪费纸张了,所有的人都用那一份语文答案和一份数学答案。张三这一思想恰好与设计模式中的享元模式相似,下面就来介绍以下享元模式。
模式介绍
享元模式(英语:Flyweight Pattern)是一种结构型设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于只是因重复而导致使用无法令人接受的大量内存的大量物件。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。
角色构成
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
在享元模式中引入了一个工厂类,它的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
UML类图
享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。
代码改造
根据上面的介绍,依照我们的案例,可以分析出语文答案和数学答案就是具体的享元类,而试卷中的答案内容就可以看作是内部状态,而学生就可以看作是外部状态。下面就是改造后的内容。
1.首先是抽象享元类:
/**
* 抽象享元类
*/
public abstract class TestAnswer {
// 试卷答案内容
private String answer;
public abstract void showAnswer(Student student);
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
}
2.两个具体享元类:
语文试卷答案类:
/**
* 具体享元类
*/
public class ChineseTestAnswer extends TestAnswer {
public ChineseTestAnswer() {
System.out.println("老师从试卷库中取出语文试卷答案带进教室");
this.setAnswer("答案:A、B、C...");
}
@Override
public void showAnswer(Student student) {
System.out.println("学生" + student.getName() + "查看语文试卷答案[" + this.getAnswer() + "]");
}
}
数学试卷类:
/**
* 具体享元类
*/
public class MathTestAnswer extends TestAnswer {
public MathTestAnswer() {
System.out.println("老师从试卷库中取出数学试卷答案带进教室");
this.setAnswer("答案:C、B、A...");
}
@Override
public void showAnswer(Student student) {
System.out.println("学生" + student.getName() + "查看数学试卷答案[" + this.getAnswer() + "]");
}
}
3.外部状态类:
/**
* 外部状态:学生类
*/
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
4.享元工厂类:
/**
* 享元工厂类
*/
public class TestAnswerFactory {
private static Map<String, TestAnswer> map = new HashMap<>();
public static TestAnswer getTestAnswer(String type) {
TestAnswer testAnswer = map.get(type);
if(testAnswer == null){
if(type.equals("chinese")){
testAnswer = new ChineseTestAnswer();
}else if (type.equals("math")){
testAnswer = new MathTestAnswer();
}else {
throw new IllegalArgumentException("输入的试卷类型不存在");
}
}
map.put(type, testAnswer);
return testAnswer;
}
}
5.测试类:
public class Main {
public static void main(String[] args) {
TestAnswer chinese = TestAnswerFactory.getTestAnswer("chinese");
TestAnswer math = TestAnswerFactory.getTestAnswer("math");
Student zs = new Student("张三");
chinese.showAnswer(zs);
math.showAnswer(zs);
System.out.println("-----------------------------------------");
Student ls = new Student("李四");
chinese.showAnswer(ls);
math.showAnswer(ls);
}
}
6.测试结果:
老师从试卷库中取出语文试卷答案带进教室
老师从试卷库中取出数学试卷答案带进教室
学生张三查看语文试卷答案[答案:A、B、C...]
学生张三查看数学试卷答案[答案:C、B、A...]
-----------------------------------------
学生李四查看语文试卷答案[答案:A、B、C...]
学生李四查看数学试卷答案[答案:C、B、A...]
可以看到经过改造后,语文和数学两个科目只用拿一份答案,张三和李四也能参照答案,同时节约了纸张。
这里的的试卷工厂就像是一个池子一样,它里面存放了我们需要的试卷答案,同时这些答案是共享的。我们的案例中不存在非共享具体享元类,所以也可以说是单纯享元模式。还有一种称之为复合享元模式,它是指将一些单纯享元对象使用组合模式加以组合,形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
模式应用
享元模式在 JDK 中的一些包装类中应用非常广泛,包括:Integer.valueof(int)
、Boolean.valueof(boolean)
、Byte.valueof(byte)
、Character.valueof(char)
、Long.valueof(long)
,下面就看一下常用的Integer
类:
1.测试代码:
public class Main {
public static void main(String[] args) {
Integer integer1 = Integer.valueOf(127);
Integer integer2 = Integer.valueOf(127);
System.out.println(integer1 == integer2);
Integer integer3 = Integer.valueOf(128);
Integer integer4 = Integer.valueOf(128);
System.out.println(integer3 == integer4);
}
}
2.测试结果:
true
false
首先我们知道在 Java 中==
判断引用类型数据时比较的是引用地址值,而看到测试结果,不知道会不会对于==
判断的到底是什么感到怀疑?首先==
对于引用数据类型确实是引用地址值,所以第二个判断结果为false
,而第一个判断结果为true
就来看看源码内部是怎样的:
首先是Integer.valueof(int)
方法:
public static Integer valueOf(int i) {
// 如果传入的值在 IntegerCache.low 和 IntegerCache.high 之间
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 就返回数组中的值
return new Integer(i); // 否则返回新的 Integer 对象
}
可以看到这三行代码很简单,如果我们传入的只在IntegerCache.low
和IntegerCache.high
之间的话,它就会返回IntegerCache.cache[i + (-IntegerCache.low)]
,而cache
是IntegerCache
类中的一个静态数组,IntegerCache
是Integer
中的私有静态内部类,其具体代码如下:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
可以看到在这个类中,做了判断,如果integerCacheHighPropValue != null
那么high
就会使用设置的值,如果没有设置值的话,缓存的最大值high
就是127。而最小值在Integer
类中有定义:static final int low = -128;
,所以就是说在-128到127之间,使用Integer.valueof(int)
返回的都是缓存中的对象,所以就可以解释integer1 == integer2
的结果为什么是true
了。其中缓存的最大值可以通过-XX:AutoBoxCacheMax=<size>
来设置。
总结
1.主要优点
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
2.主要缺点
- 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
3.适用场景
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 设计模式深入浅出--11.享元模式及其在JDK中的应用
- Java Integer的缓存策略
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/flyweight
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/1a9bebf6f85a