享元模式
从森林渲染说起
游戏中的森林有 100,000 棵树,每棵树都有自己的位置和生长阶段,但同一树种的 3D 模型、纹理贴图、粒子效果完全相同。如果每棵树都存一份完整数据,100,000 × 10KB ≈ 1GB 内存——帧率直接崩溃。
关键发现:100,000 棵树背后可能只有 10 种树型。享元模式将对象状态拆分为"内部状态"(多树共享的模型数据,只存一份)和"外部状态"(每棵树独有的坐标和生长阶段,调用时传入)。10 种共享对象 + 100,000 个只含坐标的轻量引用,内存降到可接受范围。
🔍 定义
享元模式(Flyweight)通过共享相同的细粒度对象来减少内存占用。将对象状态分为**内部状态**(可共享、不变)和**外部状态**(每次使用时传入),大量对象共用同一份内部状态对象。
⚠️ 不使用享元存在的问题
森林模拟游戏需要渲染 100,000 棵树,每棵树都存储完整数据:
| FlyweightBadExample.java |
|---|
| package com.example.structural.flyweight;
/**
* 享元模式 - 反例
* 问题:每棵树对象都独立存储纹理、颜色等重复数据,内存浪费严重
*/
public class FlyweightBadExample {
public static void main(String[] args) {
// ❌ 10000 棵树,每棵都存储相同的纹理数据
TreeBad[] forest = new TreeBad[10_000];
for (int i = 0; i < forest.length; i++) {
// 每个对象都独立持有 texture 字符串(假设很大)
forest[i] = new TreeBad(i * 10, i * 5, "oak", "#228B22", "oak_texture_data_HUGE_STRING");
}
System.out.println("❌ 创建了 " + forest.length + " 棵树,每棵都存储重复纹理数据");
System.out.println("最后一棵:" + forest[9999]);
}
}
// ❌ 外在状态和内在状态都存在同一对象里,内存浪费
class TreeBad {
// 外在状态(每棵树不同,合理)
private final int x;
private final int y;
// 内在状态(所有同类树都相同,重复!)
private final String species; // 树种
private final String color; // 颜色
private final String texture; // 纹理数据(可能很大)
public TreeBad(int x, int y, String species, String color, String texture) {
this.x = x;
this.y = y;
this.species = species;
this.color = color;
this.texture = texture;
}
@Override public String toString() {
return "Tree{x=" + x + ", y=" + y + ", species=" + species + "}";
}
}
|
🏗️ 设计模式结构说明
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class TreeType {
-name: String
-color: String
-texture: byte[]
+draw(x, y) void
}
class TreeTypeFactory {
-cache: Map~String,TreeType~
+getTreeType(name, color, texture)$ TreeType
}
class Tree {
-x: double
-y: double
-type: TreeType
+draw() void
}
Tree o--> TreeType
TreeTypeFactory ..> TreeType
note for TreeType "享元(Flyweight)"
note for TreeTypeFactory "享元工厂(FlyweightFactory)"
note for Tree "情境(Context)"
TreeType 存储可共享的内部状态(名称、颜色、纹理),Tree 只存储不可共享的外部状态(坐标),通过工厂缓存保证相同类型的 TreeType 对象只创建一次。
💻 设计模式举例说明
| FlyweightExample.java |
|---|
| package com.example.structural.flyweight;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 享元模式 - 正例
* 将"内在状态"(树种、颜色、纹理)提取为共享的 TreeType 对象,
* Tree 只保存"外在状态"(坐标),大幅降低内存占用
*/
public class FlyweightExample {
public static void main(String[] args) {
TreeTypeFactory factory = new TreeTypeFactory();
Forest forest = new Forest();
// ✅ 10000 棵橡树共享同一个 TreeType 对象
for (int i = 0; i < 10_000; i++) {
forest.plant(i * 10, i * 5, "oak", "#228B22", "oak_texture", factory);
}
// 再加 5000 棵松树,也共享各自的 TreeType
for (int i = 0; i < 5_000; i++) {
forest.plant(i * 8, i * 6, "pine", "#1B5E20", "pine_texture", factory);
}
System.out.println("✅ 树对象总数: " + forest.size());
System.out.println("✅ TreeType(共享)对象数量: " + factory.typeCount()); // 只有 2 个!
forest.get(0).draw();
}
}
// 享元对象:只存储内在状态(可共享,不随每棵树变化)
class TreeType {
private final String species; // 树种
private final String color; // 颜色
private final String texture; // 纹理(内存大户)
public TreeType(String species, String color, String texture) {
this.species = species;
this.color = color;
this.texture = texture;
}
// 渲染时接收外在状态(坐标)
public void draw(int x, int y) {
System.out.println("绘制 " + species + " 树 at (" + x + "," + y
+ ") color=" + color);
}
}
// 享元工厂:确保相同内在状态只创建一个 TreeType
class TreeTypeFactory {
private final Map<String, TreeType> cache = new HashMap<>();
public TreeType get(String species, String color, String texture) {
String key = species + "_" + color;
return cache.computeIfAbsent(key, k -> {
System.out.println("✅ 创建新 TreeType: " + key);
return new TreeType(species, color, texture);
});
}
public int typeCount() { return cache.size(); }
}
// 上下文对象:只存储外在状态(坐标)
class Tree {
private final int x;
private final int y;
private final TreeType type; // 引用共享的享元对象
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw() { type.draw(x, y); }
}
// 客户端:管理大量树对象
class Forest {
private final List<Tree> trees = new ArrayList<>();
public void plant(int x, int y, String species, String color,
String texture, TreeTypeFactory factory) {
TreeType type = factory.get(species, color, texture); // 从工厂获取共享对象
trees.add(new Tree(x, y, type));
}
public int size() { return trees.size(); }
public Tree get(int index) { return trees.get(index); }
}
|
⚖️ 优缺点
优点:
- 大幅减少大量相似对象的内存占用
- 如果外部状态合理管理,可以显著提升程序性能
缺点:
- 将状态分为内部/外部,增加了代码复杂度
- 外部状态需要由客户端传入,接口稍显繁琐
- 享元对象不能存储外部状态,多线程环境下要注意线程安全
🔗 与其它模式的关系
相似模式防混淆:
| 模式 |
意图 |
实例数量 |
| 享元(Flyweight) |
共享细粒度对象节省内存 |
多个(按类型缓存) |
| 单例(Singleton) |
保证全局唯一实例 |
恰好 1 个 |
组合使用:
享元工厂本身通常用单例管理,享元对象可以是组合树中的叶子节点(如渲染引擎中的字符对象)。
🗂️ 应用场景
- 系统中存在大量相似对象,导致内存占用过高
- 对象大部分状态可以外部化(分离为内/外部状态)
- JDK:
Integer.valueOf(-128~127) 整数缓存池;String.intern() 字符串常量池
- 游戏引擎:子弹、粒子、棋子等大量重复对象
🏭 工业视角
内部状态 vs 外部状态:享元模式的核心设计决策
享元模式的难点不在于实现,而在于**如何正确拆分内部状态与外部状态**:
| 维度 |
内部状态(Intrinsic State) |
外部状态(Extrinsic State) |
| 变化性 |
不变,创建后固定 |
随使用上下文变化 |
| 共享性 |
可被多处同时共享 |
不能共享,由调用方持有或传参 |
| 典型例子 |
棋子类型/颜色、字符格式 |
棋子坐标、字符在文档中的位置 |
拆分不当的严重后果
若把会变化的字段(如坐标)放入享元对象内部,共享的对象就会被某一调用方修改,影响所有使用它的地方——这是典型的共享状态污染 Bug,在多线程环境下尤为危险。享元对象应设计为**不可变对象**(不暴露任何 setter 方法)。
Java 标准库中的享元:Integer Cache 与 String 常量池
JDK 内置了两个经典的享元实现,理解它们有助于避免日常开发中的对比陷阱:
| Integer.valueOf 的享元缓存(-128 ~ 127) |
|---|
| // Integer.valueOf 源码(简化)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)]; // 命中缓存,返回共享对象
}
return new Integer(i); // 超出范围,创建新对象
}
Integer i1 = 56; // 自动装箱 → valueOf(56) → 命中缓存
Integer i2 = 56;
System.out.println(i1 == i2); // true ← 同一个缓存对象(享元效果)
Integer i3 = 129; // valueOf(129) → 超出范围 → new Integer(129)
Integer i4 = 129;
System.out.println(i3 == i4); // false ← 两个不同对象
|
实际开发注意事项
Integer 比较**永远用 .equals()**,不要用 ==,-128~127 的 == 看似正确只是享元的副作用
Long、Short、Byte 同样有对应范围的缓存;Double、Float 没有缓存
String.intern() 将字符串放入常量池并返回池中引用;与 IntegerCache 的区别是**按需缓存**(用到才加入),而非启动时预先全量创建
享元 vs 单例 vs 对象池:三种"复用"的本质区别
三者代码形态相似,但设计意图截然不同:
| 模式 |
解决的问题 |
实例数量 |
使用方式 |
| 享元(Flyweight) |
节省**内存空间** |
多个(按 key 缓存) |
所有调用方**同时共享**同一对象(对象不可变) |
| 单例(Singleton) |
全局唯一实例 |
恰好 1 个 |
全局共享 |
| 对象池(Pool) |
节省**对象创建时间** |
动态扩缩 |
独占使用,用完归还后才可被他人取走 |
享元与对象池的关键区别:享元对象在整个生命周期内被所有使用者**同时**共享(因此必须不可变);对象池中的对象同一时刻只被**一个**使用者独占,使用完毕归还后才能被下一个使用者取走。