享元模式核心是分离内部状态(不可变、可共享)与外部状态(可变、需传入),通过享元工厂统一管理实例,避免重复创建;适用于大量细粒度对象且状态可拆分的场景。

核心是把对象拆成「不变的共享部分」和「变化的独立部分」,让前者只存一份,后者按需传入。关键不在多写代码,而在准确识别哪些状态真能共享。
明确区分内部状态和外部状态
内部状态必须满足两个条件:不可变、不依赖上下文。比如字体名称、字号、颜色、图标资源路径——这些在整个应用生命周期里不会变,且所有用到它的对象都完全一致。外部状态则是每次使用时才确定的,比如文本坐标、显示区域、用户ID、渲染时间戳。
常见误判点:
- 把“是否启用”当作内部状态——如果不同对象开关逻辑不同,它其实是外部状态
- 把“创建时间”塞进享元类——它随实例而异,属于外部状态,应由调用方传入
- 把“语言Locale”硬编码进享元——若系统支持多语言切换,Locale应属外部状态
用工厂统一管理享元实例
享元工厂不是简单缓存,而是要控制唯一性粒度。例如:
立即学习“Java免费学习笔记(深入)”;
- 样式对象按 font+size+bold+color 四元组生成 key,避免“14号黑体”和“14号粗体”被当成同一份
- 图标对象用资源 ID(如 "ic_home_filled")作 key,而非文件路径(路径可能含版本号或CDN参数)
- 对高频创建场景(如每帧渲染),用 ConcurrentHashMap 替代 HashMap,避免并发重复构造
工厂内不保存外部状态,只负责返回已构造好的享元对象。客户端拿到后,直接调用 render(x, y, userId) 这类带参方法即可。
避免过度共享导致逻辑耦合
享元对象本身不应持有任何业务上下文。它只做一件事:用内部状态 + 外部参数完成一次操作。例如:
- ❌ 错误:享元类里存一个 UserContext 字段,并在 display() 中调用 userContext.getRole()
- ✅ 正确:display(int x, int y, String role) —— role 由上层判断后传入
- ❌ 错误:享元内部 new Date() 记录最后使用时间
- ✅ 正确:display(long renderTimestamp),时间由渲染引擎统一提供
一旦享元开始感知环境或维护状态,就违背了“无状态共享”的本质,也埋下线程安全和内存泄漏隐患。
配合弱引用防止内存长期滞留
当享元对象引用大量资源(如 Bitmap、FontRenderContext、Shader 程序句柄),且使用频率不高时,可考虑用 WeakHashMap 或 SoftReference 包装享元池。这样在 GC 压力大时,JVM 可自动回收不活跃的享元实例,而不影响核心功能。
适用信号:
- 对象体积 > 100KB
- 单个享元被复用频次低于每分钟 1 次
- 应用有明确的“前台/后台”生命周期,后台时主动清空池
注意:String 常量池、Integer 缓存等 JVM 内置享元已自动处理,无需手动干预。


















