文章目录
-
- 前言
- [1. 享元模式是什么?](#1. 享元模式是什么?)
- [2. 享元模式解决什么问题?](#2. 享元模式解决什么问题?)
- [3. 核心结构](#3. 核心结构)
-
- [3.1 Flyweight(抽象享元角色)](#3.1 Flyweight(抽象享元角色))
- [3.2 ConcreteFlyweight(具体享元角色)](#3.2 ConcreteFlyweight(具体享元角色))
- [3.3 FlyweightFactory(享元工厂 / 享元池)](#3.3 FlyweightFactory(享元工厂 / 享元池))
- [3.4 Client(客户端)](#3.4 Client(客户端))
- [4. 实现思路](#4. 实现思路)
- [5. 示例](#5. 示例)
-
- [5.1 Flyweight:字符接口(共享对象)](#5.1 Flyweight:字符接口(共享对象))
- [5.2 ConcreteFlyweight:具体字符(被复用对象)](#5.2 ConcreteFlyweight:具体字符(被复用对象))
- [5.3 FlyweightFactory:享元工厂/缓存池](#5.3 FlyweightFactory:享元工厂/缓存池)
- [5.4 Client:客户端调用(外部状态随调用变化)](#5.4 Client:客户端调用(外部状态随调用变化))
- [6. 优缺点](#6. 优缺点)
-
- [6.1 优点](#6.1 优点)
- [6.2 缺点](#6.2 缺点)
- [7. 和其他模式怎么区分?](#7. 和其他模式怎么区分?)
-
- [7.1 享元 vs 单例](#7.1 享元 vs 单例)
- [7.2 享元 vs 代理](#7.2 享元 vs 代理)
- [7.3 享元 vs 组合](#7.3 享元 vs 组合)
- [8. 适用场景](#8. 适用场景)
- [9. 总结](#9. 总结)
前言
在很多系统里,我们都会遇到同一个现象:
- 同类对象会被创建得很多(例如:相同字符、相同颜色、相同提示文本、相同菜单项...)
- 但它们的大部分状态是相同的,只是少部分状态(比如"位置、上下文、时间、用户态信息")会变化
- 对象创建和内存占用不断上涨,最后变成性能和成本问题
享元模式(Flyweight Pattern) 想解决的核心就是:
尽可能复用"已经创建过的对象",把重复的东西共享起来,只保留真正变化的部分给外部。
1. 享元模式是什么?
享元模式是一种结构型设计模式,通过"共享对象"的方式减少内存消耗。
关键点是:把对象拆成两类状态:
- 内部状态(Intrinsic State):不随外部变化,适合共享(通常是固有的、可缓存的)
- 外部状态(Extrinsic State):随上下文变化,不适合共享(例如:位置、渲染参数、用户信息等)
把相同的对象"尽量少创建",用 Key 找到已有对象复用;变化的部分由外部传入。
2. 享元模式解决什么问题?
- 应用里大量重复对象,数量可能非常大
- 重复对象中一部分状态相同
- 创建这些对象的成本(内存/时间)很高
- 能把"对象的**可变部分"**从对象内部抽出去
如果你把所有状态都放在对象里,那就很难复用了------因为每个对象都会"独一无二"。
3. 核心结构
享元模式常见的结构如下:
3.1 Flyweight(抽象享元角色)
定义共享对象需要实现的行为,并且接受外部状态参数。
内部状态在享元对象中缓存;外部状态在调用时传入。
3.2 ConcreteFlyweight(具体享元角色)
真正被缓存复用的对象类型。内部通常会存:
- 内部状态(Intrinsic State)
- 行为逻辑:使用内部状态 + 外部状态完成工作
3.3 FlyweightFactory(享元工厂 / 享元池)
负责"创建/获取享元":
- 对某个 Key(例如颜色值、字符值、样式ID)先查缓存
- 有就直接复用
- 没有就创建并放入缓存
3.4 Client(客户端)
客户端不直接 new 具体享元,而是:
- 用外部参数 + 内部Key 去工厂获取享元
- 然后把外部状态传给享元执行
4. 实现思路
实现时通常为把"可变状态"挪出去:
- 抽象一个
Flyweight接口(方法里带外部状态参数) - 写具体享元,缓存内部状态(只存可共享的部分)
- 做一个工厂(Map/缓存池)用 Key 管理享元
- 客户端通过工厂拿享元,再传外部状态执行
5. 示例
终端/画布上会画很多字符,但字符本身(内部状态)只有类型不同;位置等(外部状态)每次都不同。
- 内部状态 :字符本身(
char) - 外部状态 :绘制位置(
x, y)
5.1 Flyweight:字符接口(共享对象)
java
public interface Glyph {
void draw(int x, int y); // x,y 是外部状态
}
5.2 ConcreteFlyweight:具体字符(被复用对象)
java
public class ConcreteGlyph implements Glyph {
private final char ch; // 内部状态:字符本身(可共享)
public ConcreteGlyph(char ch) {
this.ch = ch;
}
@Override
public void draw(int x, int y) {
System.out.println("绘制字符 '" + ch + "' 到 (" + x + ", " + y + ")");
}
}
5.3 FlyweightFactory:享元工厂/缓存池
java
import java.util.HashMap;
import java.util.Map;
public class GlyphFactory {
private final Map<Character, Glyph> cache = new HashMap<>();
public Glyph getGlyph(char ch) {
// 内部状态的 Key:字符 char
return cache.computeIfAbsent(ch, k -> new ConcreteGlyph(k));
}
}
5.4 Client:客户端调用(外部状态随调用变化)
java
public class Client {
public static void main(String[] args) {
GlyphFactory factory = new GlyphFactory();
Glyph a = factory.getGlyph('A');
a.draw(10, 20);
Glyph a2 = factory.getGlyph('A');
a2.draw(100, 200);
Glyph b = factory.getGlyph('B');
b.draw(30, 40);
}
}
会发现:
'A'只会创建一次(后续复用)- 但
draw(x,y)每次都带不同位置(外部状态)
6. 优缺点
6.1 优点
- 显著减少对象数量,降低内存占用
- 减少创建开销,提升性能
- 对于"重复多、共享可能高"的场景非常有效
6.2 缺点
- 拆分内部/外部状态增加复杂度
- 客户端必须传入外部状态,接口设计要更谨慎
- 享元池可能带来管理成本(缓存、过期策略等)
- 如果内部状态设计不好,复用率会很低,甚至得不偿失
7. 和其他模式怎么区分?
7.1 享元 vs 单例
- 单例:保证"一个实例"
- 享元:保证"同一类/同一 Key 的实例尽量少",可能有多个(按 Key 缓存)
7.2 享元 vs 代理
- 代理:控制访问/延迟加载/权限等
- 享元:共享对象本体,减少重复创建与内存占用
7.3 享元 vs 组合
- 组合:树形结构统一处理
- 享元:共享对象状态减少资源消耗
8. 适用场景
- 大量对象可归类,且存在大量可共享部分
- 内部状态相对稳定,外部状态在运行中变化
- 典型如:文本编辑/渲染、图形绘制(颜色、字体、样式)、缓存规则对象、棋盘/网格格子等
如果发现对象里"重复字段很多",而"真正每次变化的字段很少",那可以使用享元模式
9. 总结
享元模式通过区分内部状态与外部状态,把大量可共享对象放入享元池复用;客户端从工厂按 Key 获取享元,再把变化的外部状态传入执行,从而显著减少内存与创建开销。
