享元模式(Flyweight Pattern)
一、概念
享元模式是一种结构型设计模式 ,核心思想是:运用共享技术有效地支持大量细粒度对象的复用,从而节省内存。
「享元」= 共享 + 元素
它将对象的状态分为两部分:
| 状态类型 | 说明 | 是否共享 |
|---|---|---|
| 内部状态(Intrinsic State) | 对象可共享的、不随环境变化的部分 | ✅ 共享 |
| 外部状态(Extrinsic State) | 随环境变化的、不可共享的部分 | ❌ 不共享,由客户端传入 |
二、结构
┌─────────────┐
│ Client │
└──────┬──────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ FlyweightFactory │──────▶│ Flyweight │ (接口/抽象类)
│ (享元工厂) │ │ + operation(外部) │
└──────────────────┘ └────────┬─────────┘
缓存池(Map) │
┌─────────┴─────────┐
│ │
┌─────────────────┐ ┌──────────────────────┐
│ ConcreteFlyweight│ │ UnsharedFlyweight │
│ (共享的享元对象) │ │ (不共享的享元对象,可选)│
└─────────────────┘ └──────────────────────┘
三、生活类比 🌳
想象一个围棋游戏:
- 棋盘上可能有上百颗棋子
- 但棋子只有 黑 和 白 两种颜色(内部状态)→ 只需要创建 2 个对象
- 每颗棋子的位置(x, y) 不同(外部状态)→ 使用时传入
四、代码示例(围棋棋子)
1. 享元接口
java
// 享元接口
public interface ChessPiece {
// 外部状态通过参数传入
void draw(int x, int y);
}
2. 具体享元类
java
// 具体享元 ------ 共享的棋子对象
public class ConcreteChessPiece implements ChessPiece {
// 内部状态:颜色(被共享,不可变)
private final String color;
public ConcreteChessPiece(String color) {
this.color = color;
System.out.println(">>> 创建了一个 [" + color + "] 棋子对象");
}
@Override
public void draw(int x, int y) {
// 外部状态:位置(由客户端传入,不共享)
System.out.println(color + "棋子落在 (" + x + ", " + y + ")");
}
}
3. 享元工厂
java
import java.util.HashMap;
import java.util.Map;
public class ChessPieceFactory {
// 缓存池:存储已创建的享元对象
private static final Map<String, ChessPiece> cache = new HashMap<>();
public static ChessPiece getChessPiece(String color) {
// 如果缓存中已有,则直接返回(共享)
if (!cache.containsKey(color)) {
cache.put(color, new ConcreteChessPiece(color));
}
return cache.get(color);
}
public static int getCacheSize() {
return cache.size();
}
}
4. 客户端使用
java
public class Client {
public static void main(String[] args) {
// 下了很多棋子,但实际只创建了 2 个对象
ChessPiece black1 = ChessPieceFactory.getChessPiece("黑");
black1.draw(3, 4);
ChessPiece white1 = ChessPieceFactory.getChessPiece("白");
white1.draw(5, 6);
ChessPiece black2 = ChessPieceFactory.getChessPiece("黑");
black2.draw(7, 8);
ChessPiece white2 = ChessPieceFactory.getChessPiece("白");
white2.draw(1, 2);
ChessPiece black3 = ChessPieceFactory.getChessPiece("黑");
black3.draw(9, 10);
// 验证共享
System.out.println("\n--- 验证 ---");
System.out.println("black1 == black2 ? " + (black1 == black2)); // true
System.out.println("white1 == white2 ? " + (white1 == white2)); // true
System.out.println("实际创建对象数: " + ChessPieceFactory.getCacheSize()); // 2
}
}
5. 运行输出
java
>>> 创建了一个 [黑] 棋子对象
黑棋子落在 (3, 4)
>>> 创建了一个 [白] 棋子对象
白棋子落在 (5, 6)
黑棋子落在 (7, 8)
白棋子落在 (1, 2)
黑棋子落在 (9, 10)
--- 验证 ---
black1 == black2 ? true
white1 == white2 ? true
实际创建对象数: 2
🎯 下了 5 颗棋子,但只创建了 2 个对象! 这就是享元模式的威力。
五、现实中的应用场景
| 场景 | 内部状态(共享) | 外部状态(不共享) |
|---|---|---|
| Java String 常量池 | 字符串值 | 引用变量 |
| Java Integer 缓存(-128~127) | 数值 | 使用位置 |
| 游戏中的粒子系统 | 粒子纹理、颜色 | 位置、速度 |
| 文本编辑器的字符渲染 | 字符字体样式对象 | 字符在文档中的位置 |
| 数据库连接池 | 连接对象 | 使用者、使用时间 |
六、优缺点
✅ 优点
- 大幅减少内存使用,避免创建大量相似对象
- 提高系统性能
❌ 缺点
- 增加系统复杂度:需要分离内部状态和外部状态
- 享元对象的内部状态必须不可变(线程安全)
- 读取外部状态会使运行时间略微增加(空间换时间的逆向,这里是时间换空间)
七、一句话总结
享元模式 = 缓存池 + 对象复用。 把不变的部分抽出来共享,把变化的部分作为参数传入,从而用少量对象表示大量实例。