享元模式和单例、原型、缓存有什么区别?
简化版
享元模式关注大量细粒度对象的共享复用;单例模式关注全局唯一实例;原型模式关注通过复制创建对象;缓存关注复用数据或结果。它们都可能减少创建成本,但设计目的和适用场景不同。
详细版
区别可以这样看:
- 享元模式:共享相同内部状态的对象,外部状态由调用方传入。
- 单例模式:确保某个类全局只有一个实例。
- 原型模式:通过克隆已有对象来创建新对象。
- 缓存:保存计算结果、查询结果或对象,避免重复获取。
享元模式可能使用缓存 Map 实现,但它更强调内部状态和外部状态拆分;缓存不一定要求这种状态拆分。
完整版教学
一、享元模式和单例模式
单例模式强调“一个类只有一个对象”。享元模式强调“同样内部状态的对象只创建一份”。
例如:
- 配置中心客户端可能是单例。
- 黑棋和白棋可以是两个享元。
- 字体样式可能有很多个享元,每种样式一个。
享元对象不一定只有一个,它通常是一组对象池。对象池里每个 key 对应一个共享实例。
二、享元模式和原型模式
原型模式用于复制对象,适合创建成本高、结构复杂的对象。它的结果通常是新对象。
享元模式用于共享对象,适合复用相同内部状态。它的结果通常是已有对象。
对比:
| 对比点 | 享元模式 | 原型模式 |
|---|---|---|
| 核心动作 | 共享 | 克隆 |
| 目标 | 减少重复对象 | 简化复杂对象创建 |
| 对象关系 | 多处引用同一对象 | 得到新的独立对象 |
| 状态处理 | 拆分内部/外部状态 | 处理深浅拷贝 |
如果对象需要独立修改,用原型更合适;如果对象应该共享且不可变,用享元更合适。
三、享元模式和缓存
享元模式经常借助缓存实现,例如用 Map 保存享元对象。但缓存是更宽泛的概念。
缓存可以缓存:
- 数据库查询结果。
- HTTP 响应。
- 计算结果。
- 文件内容。
- 业务对象。
享元模式缓存的是可共享的内部状态对象,并且会把外部状态剥离出去。
如果没有内部状态和外部状态拆分,只是为了避免重复查询,那通常是缓存,不是典型享元。
四、享元模式和对象池
对象池复用对象实例,常见于数据库连接池、线程池等。对象池里的对象通常有借出、归还、生命周期管理。
享元对象通常不需要借还,多个客户端可以同时共享同一个不可变对象。
连接对象一般带有可变状态和资源句柄,所以连接池不等同于享元模式。它们都复用对象,但复用语义不同。
五、为什么这些模式容易混淆
它们都可能带来“少创建对象”的效果,所以容易被混在一起。
但设计模式不能只看效果,要看核心意图:
- 单例解决唯一性。
- 原型解决复制创建。
- 缓存解决重复获取。
- 享元解决大量相似对象的共享。
享元最独特的标志是内部状态和外部状态的拆分。
六、面试回答建议
面试中可以这样答:
享元模式不是单纯缓存,也不是单例。它通常维护一组共享对象,每个对象对应一种内部状态;外部状态在调用时传入。单例强调全局唯一,原型强调复制创建,缓存强调复用结果,享元强调细粒度对象共享和状态拆分。
这个回答能把几个容易混淆的模式区分开。
七、常见误区与追问
四者都可能减少创建,却控制不同关系。单例控制某类全局只有1个实例;享元按状态键共享 K 个实例;原型复制已有对象得到新实例;缓存按键保存结果并允许失效。比如颜色享元可能有16个,显然不是单例,也不会像原型那样每次复制。
| 检查维度 | 判定依据 |
|---|---|
| 单例 / 享元 | 全局一个 / 按内部状态共享多个 |
| 原型 / 缓存 | 复制新对象 / 保存并复用结果 |
Singleton=1;Flyweight=K;Prototype=request次副本
记忆钩子:单例管数量,享元管共享,原型管复制,缓存管命中。
- 误区:享元工厂本身是单例才算享元。 工厂生命周期不决定模式,关键是按内部状态复用对象。
- 追问:享元缓存与普通缓存有何交集? 工厂常用 Map 实现,但享元对象要求明确共享语义和状态拆分。
- 误区:原型和享元都在复用同一个实例。 原型复用初始化方案并产生副本,享元直接共享实例。
- 追问:缓存失效适用于享元吗? 键空间无界时可以淘汰,但仍在用的对象引用不应因此失效。
- 追问:如何用身份数量区分? 问同一键是返回同一对象、一个新副本,还是可能过期的新结果。
八、加强记忆
区分享元、单例、原型和缓存时抓住关键词:享元共享类型状态,单例保证唯一对象,原型复制新对象,缓存保存复用结果。享元最不能丢的是“内部状态/外部状态”。