享元模式(Flyweight Pattern)
享元模式是结构型设计模式的重要成员,其核心目标是通过共享技术有效支持大量细粒度对象的复用,从而减少内存占用和提高系统性能。它如同现实中的 “活字印刷术”—— 每个汉字(细粒度对象)被制成一个活字(共享对象),重复使用于不同的文章中,无需为每个出现的汉字重新制作活字。
一、享元模式核心概念(通用)
1. 定义
运用共享技术有效地支持大量细粒度对象的复用。系统只创建少量的同类对象,而这些对象可被多个场景共享,通过区分 “内部状态” 和 “外部状态” 实现共享。
2. 意图
- 减少对象创建数量:针对大量相似的细粒度对象,通过共享避免重复创建,降低内存消耗。
- 提高系统性能:减少对象实例化带来的资源开销(如内存分配、GC 压力)。
- 平衡内存与复杂度:通过分离 “可共享状态” 和 “不可共享状态”,在共享对象的同时保持场景灵活性。
3. 核心状态划分
享元模式的关键是区分对象的两种状态,这是实现共享的基础:
- 内部状态(Intrinsic State): 对象固有的、可共享的状态,不依赖于外部环境,一旦创建后不会改变(如字符的 “字形”)。
- 外部状态(Extrinsic State): 依赖于外部环境的、不可共享的状态,随场景变化而变化,由客户端在使用时传入(如字符的 “位置坐标”)。
4. 通用核心组件
享元模式通过 “工厂管理共享对象”“分离内外状态” 实现,包含 4 个核心角色:
| 角色名称 | 职责描述 |
|---|---|
| Flyweight(抽象享元角色) | 定义享元对象的接口,声明接收外部状态的方法(如 operation(extrinsicState))。 |
| ConcreteFlyweight(具体享元角色) | 实现抽象享元接口,存储内部状态(可共享),在 operation() 中结合外部状态完成业务逻辑。 |
| UnsharedConcreteFlyweight(非共享具体享元角色) | 部分场景中存在无法共享的细粒度对象,此类对象不参与共享,但其结构可与享元一致(可选角色)。 |
| FlyweightFactory(享元工厂角色) | 核心管理角色,负责创建和缓存享元对象: - 当客户端请求享元时,先检查缓存中是否存在; - 存在则直接返回,不存在则创建新对象并缓存; - 通常使用哈希表(如 HashMap)存储 “内部状态标识” 与 “享元对象” 的映射。 |
二、享元模式详细解析(以 “文字处理系统” 为例)
以 “文字处理软件(如 Word)” 为场景:文档中可能包含大量重复字符(如字母 'A' 可能出现上千次),若每个字符都创建独立对象,会消耗大量内存。享元模式通过共享相同字符的对象(内部状态为 “字符值”),仅在使用时传入位置等外部状态,实现高效复用。
1. 结构
- Flyweight(抽象享元):
Character,定义display(position)方法(position为外部状态)。 - ConcreteFlyweight(具体享元):
Letter,存储内部状态charValue(如 'A'),在display()中结合位置参数显示字符。 - FlyweightFactory(享元工厂):
CharacterFactory,用HashMap缓存Letter对象,键为字符值(如 'A'),值为对应的Letter实例。 - 客户端:通过工厂获取字符对象,传入位置参数调用
display()。
2. 类图(Mermaid)
- Flyweight: 享元对象
- IntrinsicState: 内部状态,享元对象共享内部状态
- ExtrinsicState: 外部状态,每个享元对象的外部状态不同

classDiagram
%% 抽象享元角色:字符接口
class Character {
<<Interface>>
+display(position: Position): void // 接收外部状态(位置)并显示
}
%% 具体享元角色:字母(共享对象)
class Letter {
-charValue: char // 内部状态(字符值,可共享)
+Letter(charValue: char)
+display(position: Position): void // 结合外部状态(位置)显示
}
%% 外部状态:位置(客户端传入,不可共享)
class Position {
-x: int // X坐标
-y: int // Y坐标
+Position(x: int, y: int)
+getX(): int
+getY(): int
}
%% 享元工厂:管理字符享元对象
class CharacterFactory {
-flyweights: HashMap~char, Character~ // 缓存享元对象
+getCharacter(charValue: char): Character // 获取或创建享元
}
%% 客户端:使用享元的场景
class Client {
+useCharacters(): void
}
%% 关系
%% 具体享元实现抽象享元
Character <|-- Letter
CharacterFactory o-- Character : 缓存(管理)
Client --> CharacterFactory : 获取享元
Client --> Position : 创建外部状态
Client --> Character : 调用display()传入外部状态
3. 时序图(Mermaid)
以 “客户端显示两个 'A' 字符(位置不同)” 为例,展示享元模式的调用流程:
sequenceDiagram
participant Client(客户端)
participant CharacterFactory(享元工厂)
participant Letter(具体享元:'A')
participant Position(外部状态:位置1)
participant Position2(外部状态:位置2)
%% 1. 客户端首次请求字符'A'
Client->>CharacterFactory: getCharacter('A')
CharacterFactory->>CharacterFactory: 检查缓存(无'A')
CharacterFactory->>Letter: new Letter('A') // 创建新享元
CharacterFactory->>CharacterFactory: 缓存'A' -> Letter对象
CharacterFactory-->>Client: 返回Letter('A')
%% 2. 客户端传入位置1显示'A'
Client->>Position: new Position(10, 20) // 创建外部状态
Client->>Letter: display(position)
Letter-->>Client: 显示 "A at (10,20)"
%% 3. 客户端再次请求字符'A'
Client->>CharacterFactory: getCharacter('A')
CharacterFactory->>CharacterFactory: 检查缓存(有'A')
CharacterFactory-->>Client: 返回缓存的Letter('A')
%% 4. 客户端传入位置2显示'A'
Client->>Position2: new Position(30, 40) // 创建新外部状态
Client->>Letter: display(position2)
Letter-->>Client: 显示 "A at (30,40)"
4. 优点
- 减少对象数量:相同对象只创建一次并共享,大幅降低内存消耗(如 1000 个 'A' 字符仅需 1 个对象)。
- 提高性能:减少对象实例化和垃圾回收(GC)的开销,尤其适用于频繁创建大量相似对象的场景。
- 分离内外状态:内部状态集中管理,外部状态由客户端控制,灵活适应不同场景。
5. 缺点
- 系统复杂度增加:需分离内外状态,引入享元工厂管理缓存,理解和维护成本提高。
- 外部状态管理成本:外部状态需由客户端传入,若外部状态复杂,可能导致客户端代码繁琐。
- 线程安全风险:若共享的享元对象内部状态可被修改(违背不可变原则),多线程环境下可能出现状态混乱。
三、Java 代码实现(文字处理系统示例)
1. 外部状态(Position)
存储字符的位置信息,由客户端创建并传入:
// 外部状态:字符位置(不可共享,随场景变化)
public class Position {
private final int x; // X坐标
private final int y; // Y坐标
public Position(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
2. 抽象享元(Character)
定义字符的抽象接口,声明接收外部状态的方法:
// 抽象享元:字符接口
public interface Character {
// 显示字符,接收外部状态(位置)
void display(Position position);
}
3. 具体享元(Letter)
实现抽象接口,存储内部状态(字符值),结合外部状态完成显示:
// 具体享元:字母(共享对象,存储内部状态)
public class Letter implements Character {
// 内部状态:字符值(如'A',可共享,创建后不可变)
private final char charValue;
// 构造器初始化内部状态
public Letter(char charValue) {
this.charValue = charValue;
}
// 结合外部状态(位置)显示字符
@Override
public void display(Position position) {
System.out.printf("字符 '%c' 显示在位置 (%d, %d)%n",
charValue, position.getX(), position.getY());
}
}
4. 享元工厂(CharacterFactory)
管理享元对象的创建和缓存,确保相同内部状态的对象只存在一个:
import java.util.HashMap;
import java.util.Map;
// 享元工厂:管理字符享元的创建与缓存
public class CharacterFactory {
// 缓存享元对象:键为内部状态(字符值),值为享元对象
private final Map<Character, Character> flyweights = new HashMap<>();
// 获取享元:存在则返回缓存,不存在则创建并缓存
public Character getCharacter(char charValue) {
// 检查缓存
Character character = flyweights.get(charValue);
// 若不存在,创建新对象并缓存
if (character == null) {
character = new Letter(charValue);
flyweights.put(charValue, character);
System.out.printf("创建新字符享元:'%c'(当前缓存大小:%d)%n",
charValue, flyweights.size());
} else {
System.out.printf("复用字符享元:'%c'(当前缓存大小:%d)%n",
charValue, flyweights.size());
}
return character;
}
}
5. 客户端(Client)
通过工厂获取享元对象,传入外部状态使用:
// 客户端:使用字符享元
public class Client {
public static void main(String[] args) {
// 创建享元工厂
CharacterFactory factory = new CharacterFactory();
// 场景1:显示文档中的字符(多次出现相同字符)
Character a1 = factory.getCharacter('A');
a1.display(new Position(10, 20)); // A at (10,20)
Character b = factory.getCharacter('B');
b.display(new Position(15, 20)); // B at (15,20)
Character a2 = factory.getCharacter('A'); // 复用已创建的'A'
a2.display(new Position(30, 40)); // A at (30,40)
Character a3 = factory.getCharacter('A'); // 再次复用
a3.display(new Position(50, 60)); // A at (50,60)
}
}
6. 输出结果
创建新字符享元:'A'(当前缓存大小:1)
字符 'A' 显示在位置 (10, 20)
创建新字符享元:'B'(当前缓存大小:2)
字符 'B' 显示在位置 (15, 20)
复用字符享元:'A'(当前缓存大小:2)
字符 'A' 显示在位置 (30, 40)
复用字符享元:'A'(当前缓存大小:2)
字符 'A' 显示在位置 (50, 60)
四、适用环境
享元模式适用于以下场景,核心判断标准是 “存在大量相似的细粒度对象,且这些对象大部分状态可共享”:
- 大量细粒度对象场景:系统需要创建大量同类对象(如文档中的字符、游戏中的粒子、地图上的树木),导致内存占用过高。
- 对象大部分状态可共享:对象的状态可分为 “内部状态”(可共享)和 “外部状态”(不可共享),且内部状态占比高。
- 对象创建成本高:对象实例化消耗大量资源(如内存、CPU),且存在重复创建相同对象的情况。
- 需要缓存池管理:希望通过缓存机制复用对象,减少重复创建(如数据库连接池、线程池)。
五、模式分析
1. 核心本质
享元模式的本质是 “共享复用 + 状态分离”:
- 共享复用:通过工厂缓存相同内部状态的对象,避免重复创建,降低内存消耗。
- 状态分离:将对象状态拆分为 “内部(共享)” 和 “外部(非共享)”,使共享对象能适应不同场景。
2. 与其他模式的区别
| 模式 | 核心差异 | 典型场景 |
|---|---|---|
| 享元模式 | 共享细粒度对象,分离内外状态,减少内存 | 字符复用、粒子系统 |
| 单例模式 | 确保全局只有一个对象实例 | 全局配置、工具类 |
| 原型模式 | 通过复制创建对象,避免重复初始化 | 复杂对象的快速创建 |
| 工厂模式 | 封装对象创建逻辑,不强调共享 | 复杂对象的创建管理 |
3. 关键设计原则
- 内部状态不可变:具体享元的内部状态应在创建后不可修改(如
Letter的charValue用final修饰),确保多线程环境下的安全性。 - 外部状态由客户端管理:外部状态不应存储在享元对象中,而应在使用时由客户端传入,避免共享对象被污染。
- 工厂缓存策略:享元工厂的缓存实现(如 HashMap)应高效,可根据需求扩展为 LRU 等淘汰策略(适用于内存有限的场景)。
六、模式扩展
享元模式可根据场景需求扩展出以下变体:
1. 复合享元模式(Composite Flyweight)
当享元对象可组合成更复杂的结构(如 “单词” 由多个 “字符” 组成),且组合后的对象也可共享时,可引入复合享元:
CompositeCharacter(复合享元):实现Character接口,包含多个Character子享元(如字母);- 工厂同时管理简单享元和复合享元,客户端可直接获取 “单词” 等组合对象。
2. 带缓存淘汰策略的享元
当内存有限或对象数量极多时,享元工厂可采用 LRU(最近最少使用)、FIFO(先进先出)等策略淘汰不常用的享元,平衡内存占用:
// 示例:LRU缓存策略(简化版)
public class LRUCharacterFactory {
private final LinkedHashMap<Character, Character> flyweights =
new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Character, Character> eldest) {
return size() > 10; // 超过10个对象则淘汰最久未使用的
}
};
// ... 其他代码与普通工厂类似
}
3. 结合单例模式
享元工厂本身可设计为单例,确保系统中只有一个享元管理器,避免重复缓存相同对象:
public class SingletonCharacterFactory {
private static final SingletonCharacterFactory INSTANCE = new SingletonCharacterFactory();
private SingletonCharacterFactory() {} // 私有构造器
public static SingletonCharacterFactory getInstance() { return INSTANCE; }
// ... 缓存逻辑
}
七、模式应用(通用 + Android)
1. 通用领域应用
- 字符串常量池:Java 中字符串常量(如
"abc")存储在常量池,相同字符串复用同一对象("abc" == "abc"为true)。 - 数据库连接池:复用数据库连接对象,避免频繁创建和关闭连接(连接信息为内部状态,SQL 语句为外部状态)。
- 游戏粒子系统:游戏中大量相同类型的粒子(如火焰、雨滴)共享纹理、大小等内部状态,位置、速度为外部状态。
- 图标库:UI 框架中相同图标(如 “关闭按钮”)复用同一图标对象,位置为外部状态。
2. Android 中的应用
Android 框架大量使用享元模式优化内存和性能,尤其是在 UI 渲染和资源管理场景:
(1)TextView 与字符缓存
- 原理:
TextView渲染文字时,相同字符(如 'A')复用同一Glyph(字形)对象,字形数据为内部状态,位置为外部状态。 - 优化:避免为每个字符重复加载字形数据,减少内存占用和绘制开销。
(2)RecyclerView 缓存机制
- 原理:
RecyclerView的Recycler缓存池复用ViewHolder对象(享元),ViewHolder的布局结构为内部状态,绑定的数据为外部状态(通过onBindViewHolder传入)。 - 优化:减少
ViewHolder的频繁创建和销毁,提升列表滑动性能。
// RecyclerView缓存复用核心逻辑(简化)
public class Recycler {
// 缓存ViewHolder(享元对象)
private final SparseArray<ViewHolder> cache = new SparseArray<>();
// 获取缓存的ViewHolder,若无则创建
public ViewHolder getViewHolder(int viewType) {
ViewHolder holder = cache.get(viewType);
if (holder == null) {
holder = createViewHolder(viewType); // 创建新对象
cache.put(viewType, holder);
}
return holder;
}
}
(3)Drawable 资源复用
- 原理:Android 对相同资源 ID 的
Drawable(如R.drawable.ic_launcher)进行缓存,多个ImageView可共享同一Drawable对象,ImageView的尺寸、位置为外部状态。 - 优化:避免重复加载图片资源,减少内存消耗(尤其是大图片)。
(4)系统图标与主题资源
- 原理:系统主题中的图标(如菜单图标、按钮背景)被多个应用共享,图标资源为内部状态,应用中的显示位置为外部状态。
- 优化:减少系统资源的重复存储,提升整体运行效率。
八、总结
享元模式是优化 “大量细粒度对象” 场景的核心方案,其核心价值在于 “通过共享复用减少对象数量,平衡内存与性能”:
- 核心优势:大幅降低内存消耗,减少对象创建和 GC 开销,尤其适用于字符、粒子、UI 组件等场景。
- 适用场景:存在大量相似对象,且可分离出可共享的内部状态和场景相关的外部状态。
- 实践建议:
- 严格区分内部状态(不可变、可共享)和外部状态(可变、客户端传入);
- 享元工厂采用高效缓存(如 HashMap),必要时添加淘汰策略;
- 结合单例模式确保工厂唯一,避免重复缓存;
- 避免过度设计:若对象数量少或状态难以分离,使用享元模式可能增加复杂度。
享元模式不仅是一种设计技巧,更是一种 “资源复用” 的思维 —— 通过识别可共享的共性,减少重复消耗,在有限资源下实现系统高效运行。