享元设计模式(Flyweight Pattern)详解
一、模式概述
1.1 简介
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术来高效地支持大量细粒度对象的复用。其核心思想是:将对象的"内部状态"(可共享、不随环境变化)与"外部状态"(不可共享、随环境变化)分离,将可共享的部分提取为共享对象,从而大幅减少内存占用,提升系统性能。
"享元"是 Flyweight 的音义兼顾译名。"享"取"分享、共享"之义,"元"取"元素、要旨"之义;音节上"享(xiǎng) 元(yuán)"与 Flyweight 只是节奏上的大致呼应,并非严格音译。这一译名的核心是"分享元素"——即以共享的方式复用重复元素,从而精简内存占用。
1.2 模式别名
- 蝇量级模式(少见别名,部分早期中文资料使用)
- 轻量级模式(强调"轻"——对象轻量)
- 缓存模式(Cache Pattern,部分视角下可视作对象级缓存的特例)
1.3 所属类别
GoF(Gang of Four)23 种设计模式之一,属于结构型模式(Structural Pattern)。
二、定义与意图
2.1 官方定义
意图:运用共享技术有效地支持大量细粒度的对象。
Use sharing to support large numbers of fine-grained objects efficiently.
——《Design Patterns: Elements of Reusable Object-Oriented Software》
2.2 设计目标
| 目标 | 说明 |
|---|---|
| 减少内存占用 | 通过共享相同状态的内部数据,避免重复创建对象 |
| 降低对象数量 | 将海量对象数量压缩到可控范围 |
| 提升性能 | 减少 GC 压力,提高系统吞吐量 |
| 抽象共享机制 | 为具有相同特征的对象提供统一的复用接口 |
三、核心概念
3.1 内部状态 vs 外部状态
这是理解享元模式最关键的概念:
| 特性 | 内部状态(Intrinsic State) | 外部状态(Extrinsic State) |
|---|---|---|
| 是否可共享 | ✅ 可共享 | ❌ 不可共享 |
| 是否随环境变化 | ❌ 不变 | ✅ 变化 |
| 存储位置 | 存储在享元对象内部 | 由客户端传入或由外部容器维护 |
| 典型示例 | 字符的字体、大小、颜色 | 字符在文档中的位置 |
| 生命周期 | 与享元对象共存亡 | 随业务上下文变化 |
3.2 关键角色
享元模式通常包含以下角色:
- Flyweight(抽象享元角色):定义对象的公共接口,接收并作用于外部状态
- ConcreteFlyweight(具体享元角色):实现抽象享元接口,存储内部状态
- UnsharedConcreteFlyweight(不可共享享元角色):不需要共享的子类实现
- FlyweightFactory(享元工厂角色):创建并管理享元对象,负责池化缓存
- Client(客户端):维护外部状态,通过工厂获取享元并组合使用
💡 5 类角色用人话说:
- 抽象享元:一个接口,规定"享元能干什么"(比如接受位置/颜色这种随场景变的参数)。
- 具体享元:接口的实现者,保存不随场景变的那部分信息(字体、颜色、形状)。
- 不可共享享元:也是接口的实现者,但不进入工厂池——典型的用法是做"复合容器"(一个棋盘、一棵树),本身不需要共享,里头装着的子节点才共享。
- 享元工厂:唯一允许创建享元的地方,同一份内部状态只造一次,下次直接返。
- 客户端:拿着外部状态(位置、坐标等每次都不同的参数)去找工厂要享元,拿到后用。
简单场景下只会用到前 4 类(第 5 类"不可共享享元"留给复合结构出现时登场,见"复合享元"示例)。
📌 关于 UnsharedConcreteFlyweight(不可共享享元):这是 GoF 原著列出的第 3 种角色,但单一池化场景(如纯字符池、纯棋子池)确实不需要它。本档的"复合享元示例"是唯一完整呈现 5 个角色的代码——在那里
CompositeFlyweightImpl即扮演该角色。
四、模式结构
4.1 UML 类图
4.2 协作时序图
4.3 对象数量对比
以英文字母场景为例,假设一段文本包含 10000 个字符。如果不使用享元模式,每出现一次字符就创建一个新对象;如果使用享元模式,相同字符将共享同一个对象。
对比说明:左侧红色节点是享元前每个字符对应的独立对象(如 "Hello" 中两个
l是两个对象);右侧绿色节点是享元后的共享对象(两个l共用一个享元B3)。10000 个字符最终仅需约 52 种共享对象(区分大小写),内存占用压缩近 200 倍。
4.4 对象数量对比表
| 方案 | 对象数量级 | 内存占用 | 适用规模 |
|---|---|---|---|
| 不使用享元 | 10000 个独立对象 | 极高 | 不推荐 |
| 使用享元 | 约 26 个(不区分大小写)至 52 个(区分大小写)共享对象 | 极低 | 推荐 |
五、适用场景
5.1 典型应用场景
5.1.1 决策流程的四大判据
该流程通过四层递进筛选判断是否需要使用享元模式,每一层都在回答一个独立问题,必须按顺序排查:
5.1.2 为什么是这个顺序——四道筛子的递进逻辑
第一道筛子:业务必要性 —— 有没有「大量相似对象」?
这是享元模式的前提条件,决定"用不用得上"。
- 问的是业务的量级特征:是否存在大量对象?这些对象之间是否高度相似?
- 如果对象数量本身就少(例如 10 个以内),或者对象之间没有相似性,享元池里只能存 1 至 N 个对象,共享机制形同虚设,反而徒增工厂、缓存、键管理的开销。
- 典型反例:每台服务器只有 3 个配置项的配置文件,不需要享元。
- 典型正例:10000 个字符(只有约 26 到 52 种可能值)→ 大量相似对象,享元价值巨大。
为什么先问这一条? 因为它是最基础的"准入资格"。没有"大量相似"这个土壤,后面所有讨论都没有意义。这是一条业务侧的必要条件。
第二道筛子:技术可行性 —— 状态能不能「外部化」(能否把 bulk 剥离成 extrinsic)?
这是享元模式的技术核心,决定"用得了用不了"。
- 问的是对象的结构特征:是否能清晰地把状态拆分为「内部状态(可共享)」和「外部状态(不可共享)」?
- 享元模式的本质动作就是"分离内外状态",如果对象的状态几乎全部随环境变化(全部都是外部状态),或者状态之间耦合严重无法拆分,那么即使业务上对象再多,也没办法抽出共享部分。
- 典型反例:每个订单对象都包含用户 ID、订单号、金额、时间戳、商品列表……这些数据每个订单都不同,几乎没有可共享部分。
- 典型正例:棋子只有「颜色」是内部状态,「位置」是外部状态 → 干净利落地拆得开。
为什么排在第二? 因为只有先确认"业务上有需求",再去分析"技术上能不能实现"才有意义。这是一条技术侧的可行性条件。
第三道筛子:经济性 —— 真的「有内存压力」吗?
这是享元模式的性价比判断,决定"值不值得用"。
- 问的是当前的资源代价:即使前两条都满足,引入享元也要付出代价(工厂、键计算、线程安全、状态分离带来的复杂度)。如果当前根本没有内存压力,那么"为了优化而优化"反而会让代码更难维护。
- 典型场景:只有 1000 个对象、总占用不到 1MB → 即使可以享元,也没必要折腾。
- 典型场景:100 万棵树木、每棵占 10KB → 内存压力显著,享元收益巨大。
为什么排在最后? 因为这是一道收益评估题。只有在确认"业务有需求"+"技术能实现"之后,再去衡量"投入产出比"才有意义。如果连前提都没有,谈收益就是空谈。
第四道筛子:实施细节 —— 共享对象能否保证「线程安全」?
这是享元模式的落地约束,决定"用了之后能不能稳稳定定地跑"。
- 问的是编码条件:享元对象被多线程共享访问时,是否能保证状态一致且无并发问题?
- 享元模式的核心是"多个调用方共享同一个对象",这天然引入并发风险:内部状态如果被修改,会污染所有调用方;工厂的池化过程如果没有并发控制,可能创建出重复对象。
- 典型场景:
- 只读共享(内部状态完全不可变)→ 无需额外处理
- 可变内部状态 + 多线程 → 必须用
ConcurrentHashMap.computeIfAbsent之类的原子操作,或对工厂加锁 - 创建过程有副作用 → 同样需要同步机制
为什么排在最后? 因为前三道筛子回答的是"要不要用",第四道筛子回答的是"用了之后能不能稳住"。这是从决策走向落地的最后一关。
🧱 铁律(四筛之后的硬约束):通过这四道筛子后,内部状态必须完全不可变。一旦内部状态可写,多个调用方共享同一对象就会互相覆盖;此时就算加上锁,享元也丧失了"无锁共享"的最大收益。因此设计时就把内部状态设为
final、禁止 setter,是享元落地的前提——这条不可妥协。
5.1.3 四道筛子的逻辑关系总结
| 顺序 | 判据 | 维度 | 失败后果 | 通俗类比 |
|---|---|---|---|---|
| 1 | 大量相似对象 | 业务必要性 | 享元无对象可共享 | 修水库前先看有没有这条河 |
| 2 | 状态可外部化 | 技术可行性 | 拆不出共享部分 | 有河后看能不能筑坝 |
| 3 | 内存压力大 | 经济性 | 投入大于产出 | 筑坝前算算值不值 |
| 4 | 线程安全可控 | 实施细节 | 共享后并发问题无法收场 | 修好之后看会不会塌 |
5.1.4 常见疑问解答
疑问 1:为什么不是先问"内存压力大不大"?
因为内存压力是结果而不是原因。是否需要享元,根源在于"对象是否有共享可能性"和"业务量级是否达到需要共享",内存压力只是这两个条件满足后的一个可观察现象。倒过来问会出现这样的尴尬场景:明明业务上只有 5 个对象,硬因为"内存紧张"去享元,结果徒增复杂度。
疑问 2:第二个判据"大部分状态可外部化"——"大部分"具体指多少?
比例并非唯一指标。判定时应综合三要素:
- 可分离性:状态能否清晰地剥离为内部 + 外部?
- 不可变性:剥离出的内部状态是否不变?(把可变状态塞进共享对象会被互相改坏,触发线程安全问题)
- 复用度:剥离出的内部状态被多少调用方重复使用?
经验阈值(仅作参考):
- 内部状态占对象总体大小的 50% 以上 → 典型复用场景下能带来显著收益
- 内部状态不到 30% → 单看比例不划算,但若复用度极高(字符字形 1 万次、棋子规则 180 次)仍值得使用
判断的本质是:(内部状态大小 × 复用次数)应当远大于享元机制本身的开销(工厂、键、缓存查找)。
疑问 3:第三个判据如果选了"否",为什么是"可考虑但不必要"而不是"不适用"?
因为前两条都满足说明技术上完全可行,只是当前收益不显著。这种情况下:
- 现状下不必上享元,避免过度设计
- 但如果未来业务扩张(对象数量爆炸),可以平滑引入 → 它是一个可选的优化项
而前两个判据的"否"是结构性否定——没救,怎么改业务都救不回来,必须放弃享元思路。
疑问 4:第四道筛子过不了,是不是就完全不能用了?
不是。前三道筛子都通过、只是线程安全这一关过不了,仍然可以采用享元——只要加上同步机制(ConcurrentHashMap、锁、不可变设计等)。所以这一关的"否"路径是带条件的通过,而不是直接放弃。
一句话记忆:先看有没有量(业务必要),再看能不能拆(技术可行),然后算值不值(经济划算),最后看稳不稳(实施细节) —— 四层筛子缺一不可,顺序也不能颠倒。
5.1.5 实施要点速查
通过四大判据后,落地实施时建议按以下清单自检:
- 合理划分内部与外部状态:内部状态必须不可变(一旦可写就被多线程互相改坏);外部状态由调用方维护,且不应反向写回享元
- 工厂控制创建:使用工厂模式统一管理对象创建,避免重复对象
- 考虑线程安全:并发环境下使用
ConcurrentHashMap.computeIfAbsent或加锁;详见后文"线程安全示例" - 监控池大小:在工厂中暴露池大小,便于性能监控与调优
- 延迟初始化:结合懒加载按需创建对象,避免初始内存占用过大
- 关注池的内存滞留:工厂通常为 static/单例,会永久持有所有已创建的享元;需监控池大小是否单调上升,必要时提供
clear()接口或采用弱引用池(注意会损失命中率)
5.1.6 性能评估指标
实施后可通过以下指标评估享元效果:
| 指标 | 评估方式 |
|---|---|
| 对象数量 | 享元池大小 vs 业务对象总数(比值越小越好) |
| 内存占用 | JVM 堆内存使用量对比 |
| 创建耗时 | 工厂 get 方法平均耗时 |
| GC 频率 | Young GC 触发频率对比 |
| 命中率 | 池中对象被复用次数 / 总请求次数 |
5.2 常见业务场景
| 场景 | 内部状态 | 外部状态 |
|---|---|---|
| 文字编辑器 | 字符内容、字体、字号 | 字符在文档中的位置 |
| 游戏场景 | 树木模型、贴图、纹理 | 树木坐标 X/Y |
| 棋牌游戏 | 棋子颜色、形状 | 棋子在棋盘的位置 |
| 地图渲染 | 道路类型、材质 | 路段起点和终点 |
| 五子棋对战 | 棋子的黑白、形状 | 棋子的位置坐标 |
| HTML 渲染 | 标签类型、CSS 样式 | 标签在 DOM 中的位置 |
| GUI 控件库 | 图标外观、控件尺寸 | 控件坐标与父容器 |
六、不适用场景
- 对象数量较少,内存压力不明显
- 对象的大部分状态都是外部的,几乎没有可共享部分
- 对象身份对业务至关重要(每个对象都必须唯一)
- 共享对象会导致线程安全问题且代价巨大
七、优缺点分析
7.1 优点
7.2 缺点
- 代码复杂度提升:需要分离内部状态与外部状态,增加了设计难度
- 读取外部状态有成本:每次调用都需要传入外部状态
- 线程安全问题:共享对象可能被多线程并发访问,需要考虑同步
- 长时间持有:享元对象生命周期较长,需要注意内存泄漏
7.3 优缺点对比表
| 维度 | 优势 | 劣势 |
|---|---|---|
| 内存 | 大幅降低对象数量 | 享元对象长期驻留,需关注内存泄漏 |
| 性能 | 减少对象创建与 GC | 多线程下需加锁或用并发集合 |
| 复杂度 | 抽象出公共部分 | 每次调用需传入外部状态 |
| 维护性 | 内部状态集中易修改 | 状态划分边界模糊,调试复杂 |
八、代码实现
8.1 示例一:经典实现 —— 享元棋子类
这是 GoF 经典案例,五子棋游戏中大量相同形状的棋子(黑子/白子)。
8.1.1 抽象享元
/**
* 抽象享元角色 —— 棋子
* 定义棋子的公共行为,需要接收外部状态(位置)
*/
public interface ChessPiece {
/**
* 落子操作
* @param x 横坐标(外部状态)
* @param y 纵坐标(外部状态)
*/
void display(int x, int y);
}
8.1.2 具体享元
/**
* 具体享元角色 —— 黑色棋子
* 内部状态:颜色
*/
public class BlackChessPiece implements ChessPiece {
private final String color = "黑色";
@Override
public void display(int x, int y) {
System.out.printf("落子位置 (%d, %d),棋子颜色:%s%n", x, y, color);
}
}
/**
* 具体享元角色 —— 白色棋子
* 内部状态:颜色
*/
public class WhiteChessPiece implements ChessPiece {
private final String color = "白色";
@Override
public void display(int x, int y) {
System.out.printf("落子位置 (%d, %d),棋子颜色:%s%n", x, y, color);
}
}
8.1.3 享元工厂
import java.util.HashMap;
import java.util.Map;
/**
* 享元工厂 —— 负责创建并缓存棋子对象
*/
public class ChessPieceFactory {
// 享元池:缓存已创建的棋子
private static final Map<String, ChessPiece> FLYWEIGHT_POOL = new HashMap<>();
static {
// 预创建两种颜色的棋子(游戏中只有黑/白两种共享棋子)
FLYWEIGHT_POOL.put("black", new BlackChessPiece());
FLYWEIGHT_POOL.put("white", new WhiteChessPiece());
}
/**
* 获取棋子享元对象
*/
public static ChessPiece getChessPiece(String color) {
return FLYWEIGHT_POOL.get(color);
}
/**
* 获取当前池中对象数量(用于验证享元效果)
*/
public static int getPoolSize() {
return FLYWEIGHT_POOL.size();
}
}
8.1.4 客户端测试
public class FlyweightClient {
public static void main(String[] args) {
// 模拟一场五子棋:双方各下 100 步
for (int i = 0; i < 100; i++) {
// 黑子
ChessPiece black = ChessPieceFactory.getChessPiece("black");
black.display(i, i * 2);
// 白子
ChessPiece white = ChessPieceFactory.getChessPiece("white");
white.display(i * 3, i + 5);
}
System.out.println("总共落子数:200");
System.out.println("实际创建棋子对象数:" + ChessPieceFactory.getPoolSize());
}
}
输出结果:
落子位置 (0, 0),棋子颜色:黑色
落子位置 (0, 5),棋子颜色:白色
...
落子位置 (99, 198),棋子颜色:黑色
落子位置 (297, 104),棋子颜色:白色
总共落子数:200
实际创建棋子对象数:2
✅ 效果:200 次落子操作仅创建了 2 个棋子对象,内存占用大幅降低。
8.2 示例二:复杂实现 —— 文字编辑器字符享元
模拟文字编辑器中字符对象的享元处理,这是享元模式最经典的应用。
本例中
CharacterFlyweight同时承担"抽象享元接口"与"具体享元实现"两个角色(一个类包揽),是简化做法的常见写法,真实工程很常见。Document内部用CharPosition记录每个字符的"行列"——这就是外部状态。
8.2.1 具体享元
/**
* 具体享元 —— 字符
* 内部状态:字符值、字体、字号、颜色
*/
public class CharacterFlyweight {
private final char value; // 字符本身(内部状态)
private final String font; // 字体(内部状态)
private final int size; // 字号(内部状态)
private final String color; // 颜色(内部状态)
public CharacterFlyweight(char value, String font, int size, String color) {
this.value = value;
this.font = font;
this.size = size;
this.color = color;
}
/**
* 渲染字符 —— 外部状态是字符在文档中的位置
*/
public void render(int row, int column) {
System.out.printf("在 [%d 行 %d 列] 渲染字符 %c,字体:%s,字号:%d,颜色:%s%n",
row, column, value, font, size, color);
}
// 重写 equals 和 hashCode,用于工厂去重
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CharacterFlyweight)) return false;
CharacterFlyweight that = (CharacterFlyweight) o;
return value == that.value
&& size == that.size
&& font.equals(that.font)
&& color.equals(that.color);
}
@Override
public int hashCode() {
int result = (int) value;
result = 31 * result + font.hashCode();
result = 31 * result + size;
result = 31 * result + color.hashCode();
return result;
}
}
8.2.2 享元工厂
import java.util.HashMap;
import java.util.Map;
/**
* 字符享元工厂 —— 基于内部状态去重
*/
public class CharacterFlyweightFactory {
private static final Map<String, CharacterFlyweight> POOL = new HashMap<>();
public static CharacterFlyweight get(char value, String font, int size, String color) {
// 构造 key:内部状态组合
String key = value + "-" + font + "-" + size + "-" + color;
// 注意:此示例使用 HashMap,多线程下不安全;多线程安全版本见后文"线程安全示例"
return POOL.computeIfAbsent(key,
k -> new CharacterFlyweight(value, font, size, color));
}
public static int getPoolSize() {
return POOL.size();
}
}
8.2.3 客户端(文档)
import java.util.ArrayList;
import java.util.List;
/**
* 文档类 —— 维护外部状态(每个字符的位置)
*/
public class Document {
private final List<CharPosition> positions = new ArrayList<>();
/**
* 添加字符到文档中
*/
public void addChar(char value, String font, int size, String color, int row, int col) {
// 从工厂获取字符享元
CharacterFlyweight flyweight = CharacterFlyweightFactory.get(value, font, size, color);
positions.add(new CharPosition(flyweight, row, col));
}
/**
* 渲染整个文档
*/
public void render() {
for (CharPosition cp : positions) {
cp.flyweight.render(cp.row, cp.column);
}
}
public int getCharCount() {
return positions.size();
}
/**
* 内部类:保存字符的位置(外部状态)
*/
private static class CharPosition {
final CharacterFlyweight flyweight;
final int row;
final int column;
CharPosition(CharacterFlyweight flyweight, int row, int column) {
this.flyweight = flyweight;
this.row = row;
this.column = column;
}
}
}
8.2.4 客户端测试
public class EditorClient {
public static void main(String[] args) {
Document doc = new Document();
// 写入一段文本:Hello World!
String text = "Hello World!";
for (int i = 0; i < text.length(); i++) {
doc.addChar(text.charAt(i), "宋体", 12, "黑色", 1, i + 1);
}
// 再写一行 Hello
for (int i = 0; i < 5; i++) {
doc.addChar(text.charAt(i), "宋体", 12, "黑色", 2, i + 1);
}
doc.render();
System.out.println("\n文档字符总数:" + doc.getCharCount());
System.out.println("享元池对象数:" + CharacterFlyweightFactory.getPoolSize());
}
}
输出结果:
在 [1 行 1 列] 渲染字符 H,字体:宋体,字号:12,颜色:黑色
在 [1 行 2 列] 渲染字符 e,字体:宋体,字号:12,颜色:黑色
...
享元池对象数:9
文档字符总数:17
✅ 效果:17 个字符仅创建了 9 个享元对象(重复字符复用)。
8.3 示例三:线程安全的享元 —— 使用 ConcurrentHashMap
在多线程环境下,需要保证享元工厂的线程安全。
本节抽出一个泛型化的工厂 ThreadSafeFlyweightFactory<T>:可以套用前面任一具体享元类型,不必为每个具体类重写工厂。
8.3.1 线程安全的享元工厂
import java.util.concurrent.ConcurrentHashMap;
/**
* 线程安全的享元工厂
*/
public class ThreadSafeFlyweightFactory<T> {
private final ConcurrentHashMap<String, T> pool = new ConcurrentHashMap<>();
/**
* 获取享元对象(线程安全)
* @param key 内部状态的 key
* @param creator 当池中没有时用于创建新对象的函数
*/
public T get(String key, java.util.function.Supplier<T> creator) {
return pool.computeIfAbsent(key, k -> creator.get());
}
public int size() {
return pool.size();
}
/**
* 清理池(谨慎使用)
*/
public void clear() {
pool.clear();
}
}
使用示例:
8.3.2 客户端测试
public class ThreadSafeExample {
public static void main(String[] args) {
ThreadSafeFlyweightFactory<CharacterFlyweight> factory = new ThreadSafeFlyweightFactory<>();
// 模拟多线程并发获取享元
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
factory.get("H-宋体-12-黑色",
() -> new CharacterFlyweight('H', "宋体", 12, "黑色"));
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("并发访问后池中对象数:" + factory.size());
// 无论多少线程访问,池中只有 1 个对象
}
}
8.4 示例四:复合享元 —— 组合模式 + 享元模式
本例演示在树形结构中,叶子节点通过享元工厂共享,复合节点(树根)本身不共享。
🎯 本例是唯一演示全部 5 类角色的代码:前面的示例只有"抽象享元 / 具体享元 / 享元工厂 / 客户端"四类;本例多了不可共享享元 —— 即复合容器本身(不参与共享,但实现了同一个接口)。
📌 本例是组合模式的简化形态:仅实现了
add()与operation()两个接口。完整的 Composite Pattern 还应提供remove(CompositeFlyweight)、getChild(int)、isComposite()等接口用于遍历与结构管理;本例为聚焦享元本质而省略这些。
8.4.1 抽象享元
/**
* 抽象享元 —— 可包含子享元的复合节点
*/
interface CompositeFlyweight {
void operation(String externalState);
}
8.4.2 具体享元(叶子节点)
⚠️ 注释陷阱:LeafFlyweight 容易被误标为"不可共享享元"——实际上它是共享的 ConcreteFlyweight,由后面的叶子工厂池化复用。原代码的注释写反了,此处已修正,请读者特别留意。
/**
* 具体享元 —— 叶子节点(由后面的叶子工厂共享相同内部状态的实例)
*/
class LeafFlyweight implements CompositeFlyweight {
private final String intrinsicState; // 内部状态
public LeafFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String externalState) {
System.out.println("叶子节点:内部状态=" + intrinsicState + ",外部状态=" + externalState);
}
}
8.4.3 不可共享享元(复合容器)
import java.util.ArrayList;
import java.util.List;
/**
* 不可共享享元 —— 复合容器节点(UnsharedConcreteFlyweight)
* 注:复合容器本身不参与共享,但其叶子子节点由后面的叶子工厂池化复用
*/
class CompositeFlyweightImpl implements CompositeFlyweight {
private final List<CompositeFlyweight> children = new ArrayList<>();
public void add(CompositeFlyweight child) {
children.add(child);
}
@Override
public void operation(String externalState) {
for (CompositeFlyweight child : children) {
child.operation(externalState);
}
}
}
8.4.4 享元工厂(叶子工厂)
import java.util.HashMap;
import java.util.Map;
/**
* 叶子享元工厂 —— 池化共享叶子节点
*/
class LeafFlyweightFactory {
private static final Map<String, LeafFlyweight> POOL = new HashMap<>();
public static LeafFlyweight get(String intrinsicState) {
return POOL.computeIfAbsent(intrinsicState, LeafFlyweight::new);
}
public static int getPoolSize() {
return POOL.size();
}
}
8.4.5 客户端测试
public class CompositeFlyweightClient {
public static void main(String[] args) {
// 构造两棵树,复用相同类型的叶子节点
CompositeFlyweightImpl tree1 = new CompositeFlyweightImpl();
tree1.add(LeafFlyweightFactory.get("红色叶子"));
tree1.add(LeafFlyweightFactory.get("绿色叶子"));
tree1.add(LeafFlyweightFactory.get("红色叶子")); // 复用
CompositeFlyweightImpl tree2 = new CompositeFlyweightImpl();
tree2.add(LeafFlyweightFactory.get("黄色叶子"));
tree2.add(LeafFlyweightFactory.get("绿色叶子")); // 复用
tree2.add(LeafFlyweightFactory.get("红色叶子")); // 复用
tree1.operation("树一渲染");
tree2.operation("树二渲染");
System.out.println("叶子节点总数:6");
System.out.println("享元池中实际对象数:" + LeafFlyweightFactory.getPoolSize());
// 预期输出:3(红、绿、黄三种叶子)
}
}
九、Java 标准库中的享元模式
9.1 Integer 缓存池
Java 中 Integer.valueOf(int) 使用了享元模式:
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b); // true(缓存命中,共享对象)
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d); // false(超出缓存范围,创建新对象)
IntegerCache 源码简化:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
默认缓存范围是
-128 ~ 127,可通过 JVM 参数-XX:AutoBoxCacheMax=调整上限。📌 本例是享元模式的简化形态:Integer 没有可分离的 extrinsic 状态(整数值本身就是全部信息),客户端也无须传入位置/颜色等上下文。这种"纯 intrinsic 池"是享元模式的一个退化形态。完整形态——即客户端必须传入 extrinsic 才能使用——见 §5.2 与 §8 中的字符 Glyph、棋类棋子与森林里的树(共享树模型/棋子规则,外部传入坐标/颜色/大小)。
9.2 String 常量池
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true(共享常量池中的对象)
JVM 字符串常量池本质上就是一个享元池。
9.3 其他应用
| 类 | 内部状态 | 外部状态 |
|---|---|---|
Integer.valueOf() |
int 值 | 无 |
Long.valueOf() |
long 值 | 无 |
Boolean.valueOf() |
true/false | 无 |
Byte.valueOf() |
byte 值 | 无 |
Character.valueOf() |
0~127 的字符 | 无 |
十、与其他设计模式的对比
10.1 享元 vs 单例
| 维度 | 享元模式 | 单例模式 |
|---|---|---|
| 对象数量 | 多个共享对象(池) | 仅一个全局对象 |
| 存在形式 | 工厂 + 池管理 | 类自身控制 |
| 状态 | 区分内部/外部状态 | 通常无状态或全状态 |
| 目的 | 节约内存 | 保证全局唯一性 |
10.2 享元 vs 对象池
| 维度 | 享元模式 | 对象池 |
|---|---|---|
| 对象生命周期 | 长期持有 | 用完归还 |
| 共享方式 | 同时共享给多个调用方 | 同一时刻独占 |
| 节省内存 | 是 | 主要节省创建时间 |
| 典型应用 | 字符、棋子 | 数据库连接、线程 |
10.3 享元 vs 缓存
| 维度 | 享元模式 | 缓存 |
|---|---|---|
| 目的 | 共享对象 | 加速访问 |
| 失效策略 | 通常不失效 | LRU、TTL 等 |
| 对象获取 | 必须从工厂 | 可从缓存或源 |
| 命中率考量 | 重要(决定共享收益) | 核心指标 |
十一、享元模式的实际框架应用
11.1 应用层次架构图
11.2 JDK 中的享元分布
📌 数字依据提示:以上百分数是按"使用频度 × 公开可见性"的粗估——JDK 源码中出现频度高且开发者最常碰到的两类(Integer、String)合计占 65%。这是示意分布,并不来自任何官方文档;不同视角(例如按"复用次数 × 节省内存字节数")的分布会显著不同。
十二、总结
12.1 一句话概括
享元模式 = 共享内部状态 + 外部化可变状态 + 工厂池化缓存
12.2 模式要素回顾
12.3 适用与不适用
| 适合使用 ✅ | 不适合使用 ❌ |
|---|---|
| 文字编辑器字符对象 | 少量独立对象 |
| 游戏场景中重复元素 | 对象状态频繁变化 |
| 棋牌游戏中棋子 | 业务要求对象必须唯一 |
| 池化缓存场景 | 共享会导致严重线程问题 |
12.4 设计哲学
享元模式体现了软件设计中"以共享代替复制"的经典思想,是处理海量数据场景下性能与内存矛盾的有效武器。它提醒我们:在面对大规模对象时,识别不变性、分离可变性、引入复用层,往往是化繁为简的关键。

浙公网安备 33010602011771号