重修设计模式-结构型-享元模式
重修设计模式-结构型-享元模式
复用不可变对象,节省内存
享元模式(Flyweight Pattern)核心思想是通过共享对象方式,达到节省内存和提高性能的目的。享元对象需是不可变对象,因为它会被多处代码共享使用,要避免一处代码对享元进行了修改,影响到其他使用它的代码。
享元模式的实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建过的享元对象,来达到复用的目的。
举个例子,在线象棋游戏:
象棋可同时容纳上万个房间同时进行游戏,每个房间最基础的是象棋和棋盘。一副象棋有32个棋子,如果后每个房间都创建相同的棋子对象,就是千万级别的,对任何系统都是个挑战。
分析这个例子,棋子的字和颜色是固定的几个,跟场景无关;场景相关的只有只有棋子的坐标。那其实可以把棋子不变的部分抽取出来,设计为享元对象,让所有棋子共享,从而让每个棋子对象变得更轻量。
未使用享元模式的棋子对象:
//棋子
data class ChessPieceOld(
val id: Long,
val text: String,
val color: ChessPieceUnit.Color,
val positionX: Int,
val positionY: Int
) {
}
当对象大量创建时,几个固定的属性(id、text、color)会大量重复,冗余占用内存。这时可以将这几个属性抽出,设计为享元类,并使用带缓存的工厂来生成享元对象:
//棋子固定信息-享元类
data class ChessPieceUnit(val id: Long, val text: String, val color: Color) {
enum class Color {
RED, BLACK
}
}
//生成棋子-带缓存的创建工厂
class ChessPieceFactory {
companion object {
//享元对象池
private val chessPiece = hashMapOf(
1 to ChessPieceUnit(1, "車", ChessPieceUnit.Color.RED),
2 to ChessPieceUnit(2, "馬", ChessPieceUnit.Color.BLACK),
//...
)
fun getChessPiece(id: Int): ChessPieceUnit {
return chessPiece[id]!!
}
}
}
//棋子
data class ChessPiece(val piece: ChessPieceUnit, val positionX: Int, val positionY: Int) {
}
//棋盘
class ChessBoard() {
private val chessPieces: HashMap<Int, ChessPiece> = hashMapOf()
fun init() {
chessPieces.put(1, ChessPiece(ChessPieceFactory.getChessPiece(1), 0, 1))
chessPieces.put(2, ChessPiece(ChessPieceFactory.getChessPiece(1), 0, 1))
//...
}
fun move(chessPieceId: Int, positionX: Int, positionY: Int) {
//...
}
}
使用享元后,所有棋子的固定部分共同引用数量有限的享元对象,每个棋子对象变得更为轻量,内存中存储的冗余信息大大减少,避免了大量相似对象的开销,提高了系统资源的利用率。
注意上面的例子,使用享元模式后,棋子对象的数量并没有变化,只是对象比之前小了很多。消耗内存最多的成员变量已经被移动到很少的几个享元对象中了,这几个享元对象会被上千个情境小对象复用,无需再重复存储相同数据。
享元模式在 Java 语言中的应用
1.Java 中的字符串常量池
String 类会利用享元模式来复用相同的字符串常量,当某个字符串常量第一次被用到的时候,存储到常量池中,之后再用到的时候,直接引用常量池中已经存在的即可,不需要重复创建对象,通过下面代码可以验证。
//字符串常量池
String s1 = "秋意浓";
String s2 = "秋意浓";
String s3 = new String("秋意浓");//直接new出对象,绕过了Java的常量池优化
System.out.println(s1 == s2); //输出:true
System.out.println(s1 == s3); //输出:false
2.基本类型包装类
基本类型的包装类(如Integer 、Long、Short、Byte 等),也都利用了享元模式来缓存 -128 到 127 之间的数据。因为对于大部分应用来说这个区间是最常用的数值,如果预先创建所有值的享元对象不仅会占用大量内存,也会让类加载时间过长。
以 Integer 类型为例:
//包装类型的享元
Integer n1 = 1;
Integer n2 = 1;
Integer n3 = 129;
Integer n4 = 129;
System.out.println(n1 == n2); //输出:true
System.out.println(n3 == n4); //输出:false
因为 129 超出了享元对象池区间,所以每次会返回新的对象,两个对象地址不同,输出 false。Integer 享元部分源码如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(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() {}
}
可以通过 JDK 参数修改缓存池上限(下限不支持修改):
//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255
在使用 Java 进行编码时,要尽量避免直接 new 出包装类型或者字符类型对象,如 String s = new String("aaa")
、Integer n = new Integer(1)
,这样会绕过系统的享元模式,创建重复对象。推荐直接使用自动装箱语法,如:Integer n = 1
,或通过类型工厂的方式:Integer n = Integer.valueof(1)
上面两个场景都是享元模式在 Java 中的应用,只是实现略有不同:Integer 类中要共享的对象,是在类加载的时候就一次性创建好的;String 类的享元,是在某个字符串第一次被用到的时候,才创建并存储到常量池中的,相当于懒加载方式。
享元模式的类似设计
1.享元模式 vs 单例
虽然享元模式的实现和单例的变体多例非常相似,但它们的设计意图不同:享元模式是为了对象复用,节省内存。而多例是为了限制对象的个数。
2.享元模式 vs 对象池
虽然享元模式和对象池都是为了复用,但他们的“复用”也是不同的概念:
- 对象池中的“复用”可以理解为“重复使用”,使用时被使用者独占,使用完成后放回池中,主要目的是节省时间。
- 享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
总结
当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
原文地址:https://blog.csdn.net/weixin_39397471/article/details/142461769
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!