享元模式
享元模式的英文原名为Flyweight Pattern,又可以称之为蝇量模式。不够这里使用享元模式对其进行命名更能反映模式的用意:享元,共享元数据。享元模式以共享的方式高效的支持细颗粒的对象。
场景假设
现在我们模拟围棋对弈的场景,对弈双方每次下一个棋子,如果二人棋力相当,那么将有可能会出现棋盘被下满的场景
那通常情况下面对这样的场景我们应该如何做呢?首先新建一个棋子类,用来保存棋子的颜色和位置,示例代码如下
@Getter
@Setter
@AllArgsConstructor
public class Chess implements Chess {
private String color;
private String X;
private String Y;
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
于是我们在下棋的时候每当对弈双方下一个子时,我们就需要new一个棋子出来,如果双方把棋局下满的话,那我们就需要new几百个棋子出来,这无疑使造成了内存的浪费,于是我们对上述场景进行分析后,发现了以下几个特点
- 棋子只有黑白两种颜色,同样颜色的棋子本身并无不同
- 棋子本身并不需要知道自己的坐标
以上就是我们的场景假设,接下来我们来尝试使用享元模式来解决这个问题
模式介绍
概念引入
在具体介绍享元模式之前,我们首先来介绍两个概念
- 内部状态:储存在享元对象内部,不会随着外界的影响而改变的状态,内部状态可以共享
- 外部状态:会随着外界的影响而改变的状态,享元对象不对其进行保存,外部状态的保存应该由客户端负责
类图
在享元模式中,有四个角色
- 抽象享元类:享元类的父类,通常是一个接口或者抽象类,规定了享元类公共的方法
- 具体享元类:共享的享元类,类中提供了内部状态的储存空间
- 不共享具体享元类:不共享的享元类,同样是抽象享元类的子类,但是并不需要进行共享
- 享元工厂:生产具体享元类,并保存已生产的享元类
以上就是享元模式的概念介绍,接下来我们用享元模式来解决场景假设中的两个问题
首先我们对棋子类进行属性的分类。很明显,棋子的颜色不会随着外界环境而发生改变,所以棋子的颜色是内部状态。棋子的坐标会随着对弈双方的选择而不同,属于外部状态。于是我们对棋子类进行了更改
@Getter
@Setter
@AllArgsConstructor
public class ConcreteChess implements Chess {
private String color;
public void show(String x, String y) {
System.out.println(this.color + " X:" + x + "Y:" + y);
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
我们对棋子的内部状态进行保存,并且提供一个方法来接收外部状态,至于外部状态应该如何保存,享元模式并不关心。我们同时增加了一个棋子的工厂类,工厂类中保存已创建的棋子
public class ChessFactory {
private Map<String, Chess> chessPool = new HashMap<>();
public Chess getChess(String color) {
Chess res = chessPool.get(color);
if(res == null) {
res = new ConcreteChess(color);
chessPool.put(color, res);
return res;
}
return res;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
于是当我们使用享元模式优化过的代码时,就不会再造成内存的浪费了。
优秀享元模式案例——字符串常量池
字符串常量池就是享元模式的优秀实现,接下来我们来根据源码来分析一下字符串常量池是如何做的
Symbol* SymbolTable::lookup(const char* name, int len, TRAPS) { unsigned int hashValue = hash_symbol(name, len); int index = the_table()->hash_to_index(hashValue); Symbol* s = the_table()->lookup(index, name, len, hashValue); // Found if (s != NULL) return s; // Grab SymbolTable_lock first. MutexLocker ml(SymbolTable_lock, THREAD); // Otherwise, add to symbol to table return the_table()->basic_add(index, (u1*)name, len, hashValue, true, CHECK_NULL); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当我们调用String#intern方法时,jvm就会去字符串常量池中进行查找,如果找到则返回其引用
Symbol* SymbolTable::basic_add(int index_arg, u1 *name, int len, unsigned int hashValue_arg, bool c_heap, TRAPS) { /*----删去了一些和本帖无关的代码----*/ // Create a new symbol. Symbol* sym = allocate_symbol(name, len, c_heap, CHECK_NULL); assert(sym->equals((char*)name, len), "symbol must be properly initialized"); HashtableEntry<Symbol*, mtSymbol>* entry = new_entry(hashValue, sym); add_entry(index, entry); return sym; }
1
2
3
4
5
6
7
8
9
10
11
12
如果没找到则创建字符串的引用,将其保存到常量池,并且返回引用