一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
享元模式是一个不太常用的设计模式,享元模式即共享的单元,其意图是复用对象,节省内存,前提是享元对象是不可变对象。
当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
类图
代码实现
这里我们以围棋棋子为例,棋子有黑子和白子,我们将棋子颜色与棋子位置分离,尽可能复用对象
Location
/**
* @author Catch
* @since 2022-04-12
*/
public class Location {
private Integer x;
private Integer y;
public Location(Integer x, Integer y) {
this.x = x;
this.y = y;
}
public Integer getX() {
return x;
}
public Integer getY() {
return y;
}
}
Piece
/**
* @author Catch
* @since 2022-04-12
*/
public class Piece {
// 内部状态
private String color;
public Piece(String color) {
this.color = color;
}
// 外部状态
public void display(Location location) {
System.out.println(color + "棋子: x=" + location.getX() + ", y=" + location.getY());
}
public String getColor() {
return color;
}
}
PieceFactory
public class PieceFactory {
private static final Map<String, Piece> MAP = new HashMap<>();
public static Piece getPiece(String color) {
Piece piece = MAP.get(color);
if (piece != null) {
return piece;
}
piece = new Piece(color);
MAP.put(color, piece);
return piece;
}
}
Main
public class Main {
public static void main(String[] args) {
Piece blackPiece = PieceFactory.getPiece("黑色");
Piece whitePiece = PieceFactory.getPiece("白色");
blackPiece.display(new Location(1, 1));
whitePiece.display(new Location(2, 2));
}
}
享元模式与单例、缓存、对象池的区别
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。享元模式有点类似于单例模式的变体多例模式。但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
使用缓存,主要是为了提高访问效率,而非复用节省内存。
池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
在 JDK 中的应用
Integer
查看下面的代码,思考一下,输出结果会是什么?
Integer i1 = 18;
Integer i2 = 18;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
上面的输出结果会是
true
false
为什么会是这样?要了解这个,我们首先要了解 Java 的自动装箱(Autoboxing)和自动拆箱(Unboxing)。
当我们把一个基本数据类型赋值给一个包装类型的时候,会触发自动装箱操作,如Integer i = 35;,底层实际执行了Integer i = Integer.valueOf(35);;反之,当我们把一个包装类型赋值给一个基本数据类型的时候,会触发自动拆箱,如int j = i;,底层实际执行了int j = i.intValue();。
在 Java 中==比较的是两个对象的内存地址,因此我们首先看下装箱的过程究竟做了什么?下面是 Integer 相关的源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
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() {}
}
这里的IntegerCache就是享元模式的一种应用,它首先比较当前值是否在IntegerCache内,如果在则直接从IntegerCache的 cache 数组中取出,否则调用 new 新生产对象。IntegerCache 的static代码块在类加载的时候执行,可以看到在没有设置-XX:AutoBoxCacheMax=<size>时,其默认大小为[-128, 127]。
如果你通过分析应用的 JVM 内存占用情况,发现 -128 到 255 之间的数据占用的内存比较多,你就可以用-XX:AutoBoxCacheMax=255或-Djava.lang.Integer.IntegerCache.high=255的方式,将缓存的最大值从 127 调整到 255。不过,这里注意一下,JDK 并没有提供设置最小值的方法。
String
我们来看如下代码
String s1 = "a";
String s2 = "a";
String s3 = new String("a");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
上面代码运行后返回如下
true
false
为什么会这样呢?JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。跟Integer的设计思路类似,但是Integer是在类加载的时候一次性创建好的,但是对于字符串,我们没法知道要共享哪些字符串常量,所以没办法提前创建好,能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可。