一、模式介绍
享元模式(Flyweight)是一种对象池的实现方式,就像我们用过的线程池、数据库连接池等一样,主要是为了避免频繁创建或者销毁相同的对象,使得系统尽可能复用已有对象,从而达到节省系统内存的目的。
该模式一般会包含如下几种角色:
-
抽象享元
所有具体享元对象的父类或者接口,其中会定义修改外部状态的方法;
-
具体享元
继承抽象享元父类或者实现接口的具体的享元对象,其中包含:
- 内部状态,用来共享的不变状态;
- 外部状态,可以通过方法被外部更改的状态;
-
享元工厂
管理和创建具体享元或者非享元对象的工厂;
我们举个例子来说明享元模式的应用。
在计算机的文档中,可以输入各式各样的字符,比如字符a,在一个文档中出现的次数可能多达成百上千次,我们完全可以使用享元模式来优化字符对象a的创建,不必每出现一次就创建一个字符对象。
public interface ICharacter {
/**
* 定位字符,可以修改外部状态
* @param line 行
* @param column 列
*/
void locateCharacter(Long line, Long column);
/**
* 将字符写出来
*/
void writeCharacter();
}
@Slf4j
public class Character implements ICharacter{
/**
* 内部状态,所有的字符全部一致,初始化后无法再改变
*/
private final String color;
private final Integer size;
private final String name;
/**
* 外部状态,可以通过writeCharacter方法来改变
*/
private Long line;
private Long column;
public Character(String color, Integer size, String name){
this.color = color;
this.size = size;
this.name = name;
}
@Override
public void locateCharacter(Long line, Long column) {
this.line = line;
this.column = column;
}
@Override
public void writeCharacter() {
log.info("在文件的第{}行,第{}列,输出字符{},它的颜色是{},大小是{}", line, column, name, color, size);
}
}
@Slf4j
public class CharacterFactory {
/**
* 保存所有已经创建的字符对象
*/
private static Map<String, ICharacter> characterMap = new ConcurrentHashMap<>();
public static ICharacter getCharacter(String name){
if(characterMap.containsKey(name)){
log.info("缓存中已经存在字符{}", name);
return characterMap.get(name);
}
log.info("暂无{}这个字符对象,现在创建!", name);
ICharacter character = new Character("black", 16, name);
characterMap.put(name, character);
return character;
}
}
public class Main {
public static void main(String[] args) {
ICharacter a1 = CharacterFactory.getCharacter("a");
a1.locateCharacter(1L, 3L);
a1.writeCharacter();
ICharacter a2 = CharacterFactory.getCharacter("a");
a2.locateCharacter(2L, 4L);
a2.writeCharacter();
}
}
17:11:44.931 [main] INFO 暂无a这个字符对象,现在创建!
17:11:44.937 [main] INFO 在文件的第1行,第3列,输出字符a,它的颜色是black,大小是16
17:11:44.938 [main] INFO 缓存中已经存在字符a
17:11:44.938 [main] INFO 在文件的第2行,第4列,输出字符a,它的颜色是black,大小是16
在这个例子中,只要字符没有被创建过,就会被工厂方法创建,并缓存起来,下次如果还想要这个字符,工厂方法就会直接从缓存中取出先前的字符对象,省去了重新创建多个相同或者相似对象的资源开销。
如上例子中的a1字符和a2字符,我们认为是相似字符对象,它们除了在文档中出现的位置不同之外,其它属性完全相同,甚至如下a3字符和a1字符的位置都相同,那么它们就是相同字符。
ICharacter a3 = CharacterFactory.getCharacter("a");
a3.locateCharacter(1L, 3L);
a3.writeCharacter();
如此,即便文档中有成百上千的a字符,我们内存中只要保留一个该字符对象即可,大大节省了内存空间。
然而,如上的例子中还不是很完美,因为字符a,b,c,d,e......等等,它们都会保持有一个对象,这些对象也是相似对象,排除位置不说,它们也仅仅是name不同而已,color和font大家都是一样的,那为什么还要各自保存相同的属性呢,也会浪费内存空间,所以我们可以更近一步,将内部状态中共性的属性再抽离出来。
@Getter
public class Style {
/**
* 内部状态,所有的字符全部一致,初始化后无法再改变
*/
private final String color;
private final Integer size;
public Style(String color, Integer size) {
this.color = color;
this.size = size;
}
}
@Slf4j
public class Character implements ICharacter{
/**
* 内部状态,初始化后无法再改变
*/
private final String name;
private final Style style;
/**
* 外部状态,可以通过writeCharacter方法来改变
*/
private Long line;
private Long column;
public Character(String name, Style style){
this.name = name;
this.style = style;
}
@Override
public void locateCharacter(Long line, Long column) {
this.line = line;
this.column = column;
}
@Override
public void writeCharacter() {
log.info("在文件的第{}行,第{}列,输出字符{},它的颜色是{},大小是{}", line, column, name, style.getColor(), style.getSize());
}
}
@Slf4j
public class CharacterFactory {
/**
* 保存所有已经创建的字符对象
*/
private static Map<String, ICharacter> characterMap = new ConcurrentHashMap<>();
/**
* 全局只有一个Style对象
*/
private static Style characterStyle = new Style("black", 16);
public static ICharacter getCharacter(String name){
if(characterMap.containsKey(name)){
log.info("缓存中已经存在字符{}", name);
return characterMap.get(name);
}
log.info("暂无{}这个字符对象,现在创建!", name);
ICharacter character = new Character(name, characterStyle);
characterMap.put(name, character);
return character;
}
}
如上例子中,我们把内部状态中共性的属性抽到了一个新的类Style中,并且在工厂方法中初始化一个全局的Style对象,如此所有的字符对象关于color和size属性的值都使用该全局的Style对象,更加节省了内存空间。
二、使用案例
2.1 String中的享元模式应用
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
// 对比内存地址来判断是否是一个对象
// true
log.info("s1==s2?{}", s1==s2);
String s3 = "hel" + "lo";
// true
log.info("s1==s3?{}", s1==s3);
String s4 = new String("hello");
// false
log.info("s1==s4?{}", s1==s4);
String s5 = "hel" + new String("lo");
// false
log.info("s1==s5?{}", s1==s5);
String s6 = "hel";
String s7 = "lo";
String s8 = s6 + s7;
// false
log.info("s1==s8?{}", s1==s8);
String s9 = new String("hello").intern();
// true
log.info("s1==s9?{}", s1==s9);
}
当以字面量形式创建字符串对象时,JVM会在编译期将其放到字符串常量池中,在Java启动时就加载到内存中了,这些字符串常量有且只有一份。
2.2 包装类中的享元模式应用
我们以Integer为例进行说明:
@Slf4j
public class IntegerTest {
public static void main(String[] args) {
Integer a1 = Integer.valueOf(100);
Integer a2 = 100;
// true
log.info("a1==a2?{}", a1==a2);
Integer b1 = Integer.valueOf(1000);
Integer b2 = 1000;
// false
log.info("b1==b2?{}", b1==b2);
}
}
之所以有这样的结果,是因为包装类Integer对某些常用范围内的整数进行了缓存:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
所以,在缓存范围内的整数,都是直接从缓存中取出,而非创建一个新的对象。如此可以大大节省内存空间。
同理,其它类型的包装类也都有类似的机制。
三、模式总结
3.1 优点
- 可以减少相同/相似对象的创建和销毁,节省了内存开销;
3.2 缺点
- 需要区分内部状态和外部状态,增加了代码的复杂性;
网友评论