享元模式

享元模式的英文原名为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

于是我们在下棋的时候每当对弈双方下一个子时,我们就需要new一个棋子出来,如果双方把棋局下满的话,那我们就需要new几百个棋子出来,这无疑使造成了内存的浪费,于是我们对上述场景进行分析后,发现了以下几个特点

  1. 棋子只有黑白两种颜色,同样颜色的棋子本身并无不同
  2. 棋子本身并不需要知道自己的坐标

以上就是我们的场景假设,接下来我们来尝试使用享元模式来解决这个问题

模式介绍

概念引入

在具体介绍享元模式之前,我们首先来介绍两个概念

  • 内部状态:储存在享元对象内部,不会随着外界的影响而改变的状态,内部状态可以共享
  • 外部状态:会随着外界的影响而改变的状态,享元对象不对其进行保存,外部状态的保存应该由客户端负责

类图

在享元模式中,有四个角色

  • 抽象享元类:享元类的父类,通常是一个接口或者抽象类,规定了享元类公共的方法
  • 具体享元类:共享的享元类,类中提供了内部状态的储存空间
  • 不共享具体享元类:不共享的享元类,同样是抽象享元类的子类,但是并不需要进行共享
  • 享元工厂:生产具体享元类,并保存已生产的享元类

以上就是享元模式的概念介绍,接下来我们用享元模式来解决场景假设中的两个问题

首先我们对棋子类进行属性的分类。很明显,棋子的颜色不会随着外界环境而发生改变,所以棋子的颜色是内部状态。棋子的坐标会随着对弈双方的选择而不同,属于外部状态。于是我们对棋子类进行了更改

@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

我们对棋子的内部状态进行保存,并且提供一个方法来接收外部状态,至于外部状态应该如何保存,享元模式并不关心。我们同时增加了一个棋子的工厂类,工厂类中保存已创建的棋子

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

于是当我们使用享元模式优化过的代码时,就不会再造成内存的浪费了。

优秀享元模式案例——字符串常量池

字符串常量池就是享元模式的优秀实现,接下来我们来根据源码来分析一下字符串常量池是如何做的

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

如果没找到则创建字符串的引用,将其保存到常量池,并且返回引用