首页 享元模式
文章
取消

享元模式

享元模式原理与实现

定义

所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是 享元对象是不可变对象。

具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

定义中的“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

实现

以一个象棋游戏为例,一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。在不使用享元模式之前,代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ChessPiece {//棋子
  private int id;
  private String text;
  private Color color;
  private int positionX;
  private int positionY;
  public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
    this.id = id;
    this.text = text;
    this.color = color;
    this.positionX = positionX;
    this.positionY = positionX;
  }
  public static enum Color {
    RED, BLACK
  }
  // ...省略其他属性和getter/setter方法...
}

public class ChessBoard {//棋局
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
  public ChessBoard() {
        init();
  }
  private void init() {
    chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
    chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
    //...省略摆放其他棋子的代码...
  }
  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

由于我们需要给每个房间都创建一个 ChessBoard 棋局对象,但游戏大厅往往会有成千上万的房间,甚至于上百万,这样的实现将消耗大量的内存,那么如何节省内存实现对象的复用呢?

享元模式就派上用场了,像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 享元类
public class ChessPieceUnit {
  private int id;
  private String text;
  private Color color;
  public ChessPieceUnit(int id, String text, Color color) {
    this.id = id;
    this.text = text;
    this.color = color;
  }
  public static enum Color {
    RED, BLACK
  }
  // ...省略其他属性和getter方法...
}

public class ChessPieceUnitFactory {
  private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }
  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}
public class ChessPiece {
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;
  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
  // 省略getter、setter方法
}

public class ChessBoard {
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
  public ChessBoard() {
    init();
  }
  private void init() {
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(1), 0,0));
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }
  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

在这种实现下所有的 ChessBoard 对象将共享这 30 个 ChessPieceUnit 对象,从数据来看为了记录 1 万个棋局在使用享元模式之前我们需要创建 30 万个 ChessPieceUnit 对象,使用之后之创建了 30 个享元对象供所有棋局共享使用即可,大大节省了内存。

享元模式 vs 单例、缓存、对象池

享元模式与单例

享元对象类似多例,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

享元模式与缓存

  1. 复用的意图是多个子系统共用一个/多个对象
  2. 缓存对于同一份数据会在不同的地方拷贝多份,以便快速获取

享元模式与对象池

  1. 池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。
  2. 享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

什么是对象池 为了避免频繁地进行对象创建和释放导致内存碎片,我们预先申请一片连续的内存空间,也就是这里说的对象池。每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。

享元模式在 Integer 和 String 中的应用

享元模式与 Integer

Integer 实际上就使用到了享元模式,它的工厂类是 IntegerCache,IntegerCache 缓存了 -128~127 的 Integer 对象,在这个范围内的 Integer 数据最终都指向同一个对象

在我们平时的开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种。

1
2
3
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);

在后两种情况下会使用到 IntegerCache,能达到节省内存的目的。

享元模式与 String

相对于工厂类,JVM 专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”

1
2
3
4
5
String s1 = "flameking";
String s2 = "flameking";
String s3 = new String("flameking");   // new 新开辟一块空间进行存放,而非放在字符串常量池当中
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false

与 Integer 类的设计不同,String 类的享元设计并非一开始就集中创建好所有的字符串常量,事实上我们没法知道哪些字符串常量是需要共享的,所以只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需要再重新创建了。

本文由作者按照 CC BY 4.0 进行授权

门面模式

模板模式