享元模式
案例
張三和李四剛剛考完期中考試的語文和數(shù)學(xué),但不是很理想。老師在課堂的講的感覺還不是很懂,所以想找老師要答案仔細(xì)看看解題過程。接下來就用程序來模擬這一過程,假設(shè)考了語文和數(shù)學(xué)兩個(gè)科目。
1.首先定義兩個(gè)試卷類:
語文試卷類:
/**
* 語文試卷答案類
*/
public class ChineseTestAnswer {
// 試卷答案內(nèi)容
private String answer;
public ChineseTestAnswer() {
System.out.println("老師從試卷庫中取出語文試卷答案帶進(jìn)教室");
this.answer = "答案:A、B、C...";
}
public String showAnswer() {
return this.answer;
}
}
數(shù)學(xué)試卷類:
/**
* 數(shù)學(xué)書卷答案類
*/
public class MathTestAnswer {
// 試卷答案內(nèi)容
private String answer;
public MathTestAnswer() {
System.out.println("老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室");
this.answer = "答案:C、B、A...";;
}
public String showAnswer() {
return this.answer;
}
}
2.然后是學(xué)生類:
/**
* 學(xué)生類
*/
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("學(xué)生" + this.name + "查看語文試卷答案[" + chineseTestAnswer.showAnswer() + "]");
}
public void showMathTestAnswer() {
System.out.println("學(xué)生" + this.name + "查看語文試卷答案[" + mathTestAnswer.showAnswer() + "]");
mathTestAnswer.showAnswer();
}
public void setChineseTestAnswer(ChineseTestAnswer chineseTestAnswer) {
System.out.println("老師把語文試卷答案發(fā)給學(xué)生:" + this.name);
this.chineseTestAnswer = chineseTestAnswer;
}
public void setMathTestAnswer(MathTestAnswer mathTestAnswer) {
System.out.println("老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:" + this.name);
this.mathTestAnswer = mathTestAnswer;
}
}
3.測(cè)試類:
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.測(cè)試結(jié)果:
老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師把語文試卷答案發(fā)給學(xué)生:張三
學(xué)生張三查看語文試卷答案[答案:A、B、C...]
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:張三
學(xué)生張三查看語文試卷答案[答案:C、B、A...]
-----------------------------------------
老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師把語文試卷答案發(fā)給學(xué)生:李四
學(xué)生李四查看語文試卷答案[答案:A、B、C...]
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:李四
學(xué)生李四查看語文試卷答案[答案:C、B、A...]
從這一過程可以看到,老師給每個(gè)人的每個(gè)科目都拿了考試答案,這樣張三和李四就可以愉快的參考答案仔細(xì)解決自身的問題所在了。但是作為一個(gè)以節(jié)約為榮,一浪費(fèi)為恥的張三覺得,老師給每個(gè)人的各個(gè)科目都發(fā)答案浪費(fèi)了紙張。他覺得只用給每個(gè)科目一份答案就好了,一個(gè)人再看語文答案的時(shí)候,另一個(gè)人可以看數(shù)學(xué)答案。這樣試卷的答案就只用有一份就可以了,而且其他同學(xué)想看,也不用在浪費(fèi)紙張了,所有的人都用那一份語文答案和一份數(shù)學(xué)答案。張三這一思想恰好與設(shè)計(jì)模式中的享元模式相似,下面就來介紹以下享元模式。
模式介紹
享元模式(英語:Flyweight Pattern)是一種結(jié)構(gòu)型設(shè)計(jì)模式。它使用共享物件,用來盡可能減少內(nèi)存使用量以及分享資訊給盡可能多的相似物件;它適合用于只是因重復(fù)而導(dǎo)致使用無法令人接受的大量內(nèi)存的大量物件。通常物件中的部分狀態(tài)是可以分享。常見做法是把它們放在外部數(shù)據(jù)結(jié)構(gòu),當(dāng)需要使用時(shí)再將它們傳遞給享元。
角色構(gòu)成
- Flyweight(抽象享元類):通常是一個(gè)接口或抽象類,在抽象享元類中聲明了具體享元類公共的方法,這些方法可以向外界提供享元對(duì)象的內(nèi)部數(shù)據(jù)(內(nèi)部狀態(tài)),同時(shí)也可以通過這些方法來設(shè)置外部數(shù)據(jù)(外部狀態(tài))。
- ConcreteFlyweight(具體享元類):它實(shí)現(xiàn)了抽象享元類,其實(shí)例稱為享元對(duì)象;在具體享元類中為內(nèi)部狀態(tài)提供了存儲(chǔ)空間。通常我們可以結(jié)合單例模式來設(shè)計(jì)具體享元類,為每一個(gè)具體享元類提供唯一的享元對(duì)象。
- UnsharedConcreteFlyweight(非共享具體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設(shè)計(jì)為非共享具體享元類;當(dāng)需要一個(gè)非共享具體享元類的對(duì)象時(shí)可以直接通過實(shí)例化創(chuàng)建。
- FlyweightFactory(享元工廠類):享元工廠類用于創(chuàng)建并管理享元對(duì)象,它針對(duì)抽象享元類編程,將各種類型的具體享元對(duì)象存儲(chǔ)在一個(gè)享元池中,享元池一般設(shè)計(jì)為一個(gè)存儲(chǔ)“鍵值對(duì)”的集合(也可以是其他類型的集合),可以結(jié)合工廠模式進(jìn)行設(shè)計(jì);當(dāng)用戶請(qǐng)求一個(gè)具體享元對(duì)象時(shí),享元工廠提供一個(gè)存儲(chǔ)在享元池中已創(chuàng)建的實(shí)例或者創(chuàng)建一個(gè)新的實(shí)例(如果不存在的話),返回新創(chuàng)建的實(shí)例并將其存儲(chǔ)在享元池中。
在享元模式中引入了一個(gè)工廠類,它的作用在于提供一個(gè)用于存儲(chǔ)享元對(duì)象的享元池,當(dāng)用戶需要對(duì)象時(shí),首先從享元池中獲取,如果享元池中不存在,則創(chuàng)建一個(gè)新的享元對(duì)象返回給用戶,并在享元池中保存該新增對(duì)象。
UML類圖

享元類的設(shè)計(jì)是享元模式的要點(diǎn)之一,在享元類中要將內(nèi)部狀態(tài)和外部狀態(tài)分開處理,通常將內(nèi)部狀態(tài)作為享元類的成員變量,而外部狀態(tài)通過注入的方式添加到享元類中。
代碼改造
根據(jù)上面的介紹,依照我們的案例,可以分析出語文答案和數(shù)學(xué)答案就是具體的享元類,而試卷中的答案內(nèi)容就可以看作是內(nèi)部狀態(tài),而學(xué)生就可以看作是外部狀態(tài)。下面就是改造后的內(nèi)容。
1.首先是抽象享元類:
/**
* 抽象享元類
*/
public abstract class TestAnswer {
// 試卷答案內(nèi)容
private String answer;
public abstract void showAnswer(Student student);
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
}
2.兩個(gè)具體享元類:
語文試卷答案類:
/**
* 具體享元類
*/
public class ChineseTestAnswer extends TestAnswer {
public ChineseTestAnswer() {
System.out.println("老師從試卷庫中取出語文試卷答案帶進(jìn)教室");
this.setAnswer("答案:A、B、C...");
}
@Override
public void showAnswer(Student student) {
System.out.println("學(xué)生" + student.getName() + "查看語文試卷答案[" + this.getAnswer() + "]");
}
}
數(shù)學(xué)試卷類:
/**
* 具體享元類
*/
public class MathTestAnswer extends TestAnswer {
public MathTestAnswer() {
System.out.println("老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室");
this.setAnswer("答案:C、B、A...");
}
@Override
public void showAnswer(Student student) {
System.out.println("學(xué)生" + student.getName() + "查看數(shù)學(xué)試卷答案[" + this.getAnswer() + "]");
}
}
3.外部狀態(tài)類:
/**
* 外部狀態(tài):學(xué)生類
*/
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.測(cè)試類:
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.測(cè)試結(jié)果:
老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
學(xué)生張三查看語文試卷答案[答案:A、B、C...]
學(xué)生張三查看數(shù)學(xué)試卷答案[答案:C、B、A...]
-----------------------------------------
學(xué)生李四查看語文試卷答案[答案:A、B、C...]
學(xué)生李四查看數(shù)學(xué)試卷答案[答案:C、B、A...]
可以看到經(jīng)過改造后,語文和數(shù)學(xué)兩個(gè)科目只用拿一份答案,張三和李四也能參照答案,同時(shí)節(jié)約了紙張。
這里的的試卷工廠就像是一個(gè)池子一樣,它里面存放了我們需要的試卷答案,同時(shí)這些答案是共享的。我們的案例中不存在非共享具體享元類,所以也可以說是單純享元模式。還有一種稱之為復(fù)合享元模式,它是指將一些單純享元對(duì)象使用組合模式加以組合,形成復(fù)合享元對(duì)象,這樣的復(fù)合享元對(duì)象本身不能共享,但是它們可以分解成單純享元對(duì)象,而后者則可以共享。
模式應(yīng)用
享元模式在 JDK 中的一些包裝類中應(yīng)用非常廣泛,包括:Integer.valueof(int) 、Boolean.valueof(boolean)、Byte.valueof(byte)、Character.valueof(char)、Long.valueof(long),下面就看一下常用的Integer類:
1.測(cè)試代碼:
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.測(cè)試結(jié)果:
true
false
首先我們知道在 Java 中==判斷引用類型數(shù)據(jù)時(shí)比較的是引用地址值,而看到測(cè)試結(jié)果,不知道會(huì)不會(huì)對(duì)于==判斷的到底是什么感到懷疑?首先==對(duì)于引用數(shù)據(jù)類型確實(shí)是引用地址值,所以第二個(gè)判斷結(jié)果為false,而第一個(gè)判斷結(jié)果為true就來看看源碼內(nèi)部是怎樣的:
首先是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)]; // 就返回?cái)?shù)組中的值
return new Integer(i); // 否則返回新的 Integer 對(duì)象
}
可以看到這三行代碼很簡單,如果我們傳入的只在IntegerCache.low和IntegerCache.high之間的話,它就會(huì)返回IntegerCache.cache[i + (-IntegerCache.low)],而cache是IntegerCache類中的一個(gè)靜態(tài)數(shù)組,IntegerCache是Integer
中的私有靜態(tài)內(nèi)部類,其具體代碼如下:
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() {}
}
可以看到在這個(gè)類中,做了判斷,如果integerCacheHighPropValue != null那么high就會(huì)使用設(shè)置的值,如果沒有設(shè)置值的話,緩存的最大值high就是127。而最小值在Integer類中有定義:static final int low = -128;,所以就是說在-128到127之間,使用Integer.valueof(int)返回的都是緩存中的對(duì)象,所以就可以解釋integer1 == integer2的結(jié)果為什么是true了。其中緩存的最大值可以通過-XX:AutoBoxCacheMax=<size>來設(shè)置。
總結(jié)
1.主要優(yōu)點(diǎn)
- 可以極大減少內(nèi)存中對(duì)象的數(shù)量,使得相同或相似對(duì)象在內(nèi)存中只保存一份,從而可以節(jié)約系統(tǒng)資源,提高系統(tǒng)性能。
- 享元模式的外部狀態(tài)相對(duì)獨(dú)立,而且不會(huì)影響其內(nèi)部狀態(tài),從而使得享元對(duì)象可以在不同的環(huán)境中被共享。
2.主要缺點(diǎn)
- 享元模式使得系統(tǒng)變得復(fù)雜,需要分離出內(nèi)部狀態(tài)和外部狀態(tài),這使得程序的邏輯復(fù)雜化。
- 為了使對(duì)象可以共享,享元模式需要將享元對(duì)象的部分狀態(tài)外部化,而讀取外部狀態(tài)將使得運(yùn)行時(shí)間變長。
3.適用場(chǎng)景
- 一個(gè)系統(tǒng)有大量相同或者相似的對(duì)象,造成內(nèi)存的大量耗費(fèi)。
- 對(duì)象的大部分狀態(tài)都可以外部化,可以將這些外部狀態(tài)傳入對(duì)象中。
- 在使用享元模式時(shí)需要維護(hù)一個(gè)存儲(chǔ)享元對(duì)象的享元池,而這需要耗費(fèi)一定的系統(tǒng)資源,因此,應(yīng)當(dāng)在需要多次重復(fù)使用享元對(duì)象時(shí)才值得使用享元模式。
參考資料
- 大話設(shè)計(jì)模式
- 設(shè)計(jì)模式Java版本-劉偉
- 設(shè)計(jì)模式深入淺出--11.享元模式及其在JDK中的應(yīng)用
- Java Integer的緩存策略
本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/flyweight
轉(zhuǎn)載請(qǐng)說明出處,本篇博客地址:http://www.itdecent.cn/p/1a9bebf6f85a