# 12 享元（结构型）

> 系统里有海量对象，大部分状态还都重复，内存扛不住。把“可共享的内在状态”抽出来复用，只给每个实例留“随它变化的外在状态”。讲清内在/外在状态的划分，以及 Java Integer 缓存这个经典享元池。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/design-patterns/flyweight
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 12 享元 · Flyweight（结构型）

结构型收官。这一篇是唯一一个**为“性能/内存”而生**的模式——当对象多到以百万计，它教你怎么把它们塞进有限的内存。

> "Flyweight“是拳击里的”蝇量级“(最轻量级)。这个名字本身就在说：让每个对象尽可能轻。

## 一、这是什么：游戏地图里的一片森林

你在做一个游戏，地图上要渲染**一百万棵树**。每棵树对象都存：名称、颜色、纹理贴图(可能几 MB)、加上自己的坐标。一百万棵 × 每棵几 MB = 内存直接爆炸。

但你冷静看一眼：这一百万棵树，其实翻来覆去就那么**几种**——松树、枫树、橡树。同一种松树，它们的名称、颜色、纹理**完全一样**，唯一不同的只是**长在哪(坐标)**。

那还存一百万份纹理干嘛？**把“松树的名称/颜色/纹理”只存一份，让所有松树共享；每棵树自己只留一个坐标 + 一个指向“松树数据”的引用。** 一百万棵树，纹理数据却只有 3 份。这就是享元。

配套代码正是这个森林：`TreeType`(名称/颜色/纹理)被共享，种 5 棵树只创建了 2 个 `TreeType`。

## 二、解决什么痛：海量对象，状态大量重复

痛点很硬核，就是内存：

- **对象数量巨大**(百万级的树、编辑器里每个字符、地图上每个图标)。
- **这些对象的状态，很大一部分是重复的**(同种树的纹理、同个字母的字形数据)。
- 如果每个对象都完整存一份，内存被大量重复数据吃光。

享元的破解思路，是把对象的状态劈成两半：

| 状态 | 叫法 | 特点 | 例子 | 存哪 |
| --- | --- | --- | --- | --- |
| **可共享的** | 内在状态(intrinsic) | 不随实例变、可复用 | 树的名称/颜色/纹理 | 存一份，共享 |
| **随实例变的** | 外在状态(extrinsic) | 每个实例都不同 | 树的坐标 (x,y) | 每个实例自己存/外部传入 |

<Callout type="info">
  **享元隔离的“会变的东西”，恰恰是通过分离“不变的东西”实现的。** 它把“到处重复的、不变的内在状态”抽出来集中共享一份，把“每个实例独有的外在状态”剥离出去。核心动作就一个：**分清哪些状态能共享、哪些不能**——这是用享元最关键、也最烧脑的一步。
</Callout>

## 三、怎么实现：享元工厂管缓存，复用内在状态

实现有两个要件：一个**不可变的享元对象**(存内在状态)，一个**享元工厂**(缓存已创建的享元，相同的直接复用)。

```go title="go/flyweight/main.go（核心）"
// 享元:内在状态(可共享),相同名称/颜色/纹理的树共用一个实例
type TreeType struct {
	Name, Color, Texture string      // ← 内在状态,不可变
}

// 享元工厂:缓存并复用,相同内在状态绝不重复创建
type TreeFactory struct {
	types map[string]*TreeType
}
func (f *TreeFactory) GetTreeType(name, color, texture string) *TreeType {
	key := name + "|" + color + "|" + texture
	if t, ok := f.types[key]; ok {
		return t                     // ← 已有,直接复用同一个对象
	}
	t := &TreeType{name, color, texture}
	f.types[key] = t                 // ← 没有,创建并缓存
	return t
}

// 树:外在状态(坐标) + 指向共享内在状态的引用
type Tree struct {
	X, Y int                         // ← 外在状态,每棵不同
	Type *TreeType                   // ← 引用共享的内在状态
}
```

结果：种了 5 棵树，`TreeType` 只造了 2 个(松树、枫树各一)，其余全是共享引用。树的数量再翻万倍，`TreeType` 还是那 2 个。**内存占用从“棵数 × 完整数据”降到“棵数 × 一个指针 + 极少数几份完整数据”。**

## 四、有哪些坑

- **划分内在/外在状态是全部难点。** 划错了，享元就失效甚至出 bug。判据：**在实例间会变的，一定是外在状态，不能塞进共享对象**；实例间恒定的，才是内在状态。上面若手滑把坐标放进 `TreeType`，那所有树就会共享同一个坐标，全叠在一个点上——灾难。
- **享元对象必须不可变。** 既然被无数实例共享，谁都不能改它——改一个等于改所有。共享对象一旦可变，就是并发和逻辑双重噩梦。**内在状态创建后只读，是铁律。**
- **只有对象数量巨大时才划算。** 享元引入了工厂、缓存、内外状态分离的复杂度。对象就几十个？这套开销纯亏，直接每个存一份更清晰。**它是“以复杂度换内存”的交易，只有内存真的紧张、对象真的海量时才成立。**
- **共享带来的线程安全。** 多线程访问同一个享元工厂的缓存 `map`，要加锁(Go 里 `map` 并发写会 panic)。共享的东西越多，并发考量越重。

## 五、开源里怎么用

- **Java `Integer.valueOf()` 的缓存池 —— 面试常客，享元实锤。** Java 对 `-128 ~ 127` 的 `Integer` 做了缓存：`Integer.valueOf(100)` 多次调用返回**同一个对象**。所以 `Integer a = 100, b = 100; a == b` 是 `true`，而 `200 == 200`(超出缓存范围)却是 `false`——这个著名的“坑”，本质就是享元池。`Boolean`、`Character`、短 `Long` 都有类似缓存。
- **字符串驻留(String Interning)**:Java 的字符串常量池、Go/Python 对字符串字面量的驻留，让内容相同的字符串共享一份存储——享元思想在语言运行时层面的落地。
- **文字排版/浏览器渲染**：一个字体里字母 `a` 的字形(glyph)轮廓数据只存一份，页面上出现一万个 `a`，共享这一份字形，各自只带位置和大小——GoF 原书举的正是文档编辑器这个例子。
- **游戏引擎**：粒子系统、植被系统、地图瓦片(tile)，海量重复元素共享网格/纹理/材质(内在状态)，各自只带变换矩阵(外在状态)。没有享元，开放世界游戏的内存根本扛不住。

---

**结构型七连，收工。** 回看这一组主线：它们全在管“怎么把对象拼成更大的结构还不别扭”——适配器/装饰器/代理/外观管“包装”，桥接管“多维度”，组合管“树”，享元管“海量共享”。

下一章进入最大的一族[行为型](https://daiw.org/manual/design-patterns/strategy)(11 个)：对象都造好、也拼好了，现在研究**它们之间怎么分工、怎么通信、怎么协作**。第一个出场的是行为型里最实用、也最能体现“面向接口编程”的——策略模式。
