1. 引言:当对象多到撑爆内存
作为一名经历过多次内存泄漏事故的老程序员,我至今还记得第一次遇到OOM(Out Of Memory)错误时的场景。那是在开发一个文档编辑器时,当用户打开超过50页的文档时,程序突然崩溃。排查后发现,每个字符都被创建为一个独立对象,包含字体、颜色、大小等属性。一个简单的"Hello World"竟然消耗了超过1MB内存!
这种场景正是享元模式(Flyweight Pattern)大显身手的地方。想象一下,如果文档中有100万个字符,而每个字符都包含完整的样式信息,内存消耗将变得极其恐怖。但实际上,文档中大量字符都是重复的——比如空格、标点、常用字母等。这些重复字符的样式信息完全可以共享,这就是享元模式的核心思想。
提示:享元模式不是万能的,它最适合处理大量相似对象的场景。当系统中存在大量重复对象,且这些对象的大部分状态可以外部化时,就该考虑使用享元模式了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 什么是享元模式
2.1 GoF的经典定义
在《设计模式:可复用面向对象软件的基础》(GoF)一书中,享元模式被定义为:
"运用共享技术有效地支持大量细粒度对象。"
这个定义看似简单,却包含了三个关键点:
- 共享技术:通过对象共享减少内存占用
- 大量:适用于对象数量庞大的场景
- 细粒度:对象本身比较小,但数量很多
2.2 现实世界的类比
理解享元模式最好的方式是通过现实世界的类比。想象一家大型印刷厂:
- 非享元方式:每次印刷都需要制作全新的铅字(相当于每次都new对象)
- 享元方式:维护一套铅字库,每次印刷重复使用相同的铅字(相当于对象共享)
显然,享元方式大大节省了资源和时间。在软件中,这种节省表现为内存占用的减少和性能的提升。
3. 享元模式的核心思想
3.1 状态分离:内部状态 vs 外部状态
享元模式的核心在于区分两种状态:
| 状态类型 | 特点 | 示例(文字编辑器) | 存储位置 |
|---|---|---|---|
| 内部状态 | 可共享、不变 | 字符内容、字体、颜色 | 享元对象内部 |
| 外部状态 | 不可共享、可变 | 字符位置、是否选中 | 由客户端维护 |
这种分离带来了几个重要优势:
- 共享的内部状态只需存储一次
- 变化的外部状态可以灵活调整
- 对象总数大幅减少
3.2 共享与复用的区别
很多初学者容易混淆享元模式和对象池模式,它们的核心区别在于:
- 享元模式:共享的是对象的状态(内部状态),同一个享元对象可以同时被多个上下文使用
- 对象池:复用的是整个对象,同一时间一个对象只能被一个客户端使用
注意:享元模式的重点不是减少new操作的次数,而是通过共享不变的部分来减少内存占用。这是很多开发者容易误解的地方。
4. 享元模式的结构
4.1 类图解析
一个标准的享元模式包含以下角色:
code复制┌─────────────┐ ┌───────────────┐
│ Client │───────▶ Flyweight │
└─────────────┘ │+operation(ext)│
└───────────────┘
△
┌────────────┴────────────┐
┌───────────────┐ ┌──────────────────┐
│ Concrete │ │ UnsharedConcrete │
│ Flyweight │ │ Flyweight │
│+intrinsicState│ │+allState │
└───────────────┘ └──────────────────┘
△
│
┌───────────────┐
│ Flyweight │
│ Factory │
│+getFlyweight()│
└───────────────┘
4.2 角色职责详解
-
Flyweight(抽象享元)
- 定义共享对象的接口
- 声明设置外部状态的方法
- 通常是接口或抽象类
-
ConcreteFlyweight(具体享元)
- 实现抽象享元接口
- 存储内部状态
- 通常结合单例模式实现
-
FlyweightFactory(享元工厂)
- 创建和管理享元对象
- 维护享元池(通常用Map实现)
- 保证享元对象的唯一性
-
UnsharedConcreteFlyweight(非共享具体享元)
- 不需要共享的子类
- 可以直接实例化使用
-
Client(客户端)
- 维护外部状态
- 通过工厂获取享元对象
5. 示例:字符系统实现
5.1 基础实现
让我们通过一个完整的文字编辑器示例来理解享元模式的实际应用。
5.1.1 享元接口
java复制public interface TextCharacter {
// 外部状态通过参数传入
void draw(int x, int y, String fontFamily);
// 获取内部状态(可选)
char getSymbol();
}
5.1.2 具体享元实现
java复制public class ConcreteTextCharacter implements TextCharacter {
private final char symbol; // 内部状态
private final int fontSize; // 内部状态
public ConcreteTextCharacter(char symbol, int fontSize) {
this.symbol = symbol;
this.fontSize = fontSize;
}
@Override
public void draw(int x, int y, String fontFamily) {
System.out.printf("绘制字符 '%c' 在位置 (%d,%d),字体:%s,大小:%d%n",
symbol, x, y, fontFamily, fontSize);
}
@Override
public char getSymbol() {
return symbol;
}
// 重写equals和hashCode以便作为Map的键
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConcreteTextCharacter that = (ConcreteTextCharacter) o;
return symbol == that.symbol && fontSize == that.fontSize;
}
@Override
public int hashCode() {
return Objects.hash(symbol, fontSize);
}
}
5.1.3 享元工厂
java复制import java.util.HashMa
