Java设计模式-享元模式
Java设计模式-享元模式
模式概述
享元模式简介
核心思想:通过共享技术高效地支持大量细粒度对象的复用,将对象的公共状态(内部状态)与私有状态(外部状态)分离,仅创建一次公共状态的对象,私有状态在使用时动态传入,从而减少内存占用并提升性能。
模式类型:结构型设计模式(关注对象的组成与复用)。
作用:
- 减少内存消耗:通过共享重复对象,避免大量相似对象的重复创建。
- 提升系统性能:降低对象创建/销毁的开销,减少垃圾回收压力。
- 简化对象管理:通过工厂类集中管理共享对象,统一控制对象的生命周期。
典型应用场景:
- 文本编辑器/IDE中的字符对象(如Word中大量重复的字母“a”,仅存储一份字符编码,位置、颜色等动态传入)。
- 游戏开发中的粒子系统(如大量相同属性的子弹、火花,共享基础属性,位置、速度等动态计算)。
- 数据库连接池(复用已创建的数据库连接,避免频繁创建/关闭连接的开销)。
- 缓存系统(如Guava Cache,通过共享缓存对象减少重复计算)。
我认为:享元模式是“对象复用”的经典实践,用空间换时间(或用共享换内存),让大量相似对象“共用一张脸,各自有不同的姿态”。
课程目标
- 理解享元模式的核心思想和经典应用场景
- 识别应用场景,使用享元模式解决功能要求
- 了解享元模式的优缺点
核心组件
角色-职责表
角色 | 职责 | 示例类名 |
---|---|---|
抽象享元角色(Flyweight) | 定义享元对象的公共接口,声明操作外部状态的方法 | Character |
具体享元角色(ConcreteFlyweight) | 实现抽象接口,存储内部状态(不可变),依赖外部状态完成操作 | ConcreteCharacter |
非共享享元角色(UnsharedFlyweight) | 不可共享的享元对象(特殊场景使用,如需要个性化配置的对象) | SpecialCharacter |
享元工厂(FlyweightFactory) | 管理享元对象的创建与共享,确保相同内部状态的对象仅创建一次 | CharacterFactory |
类图
下面是一个简化的类图表示,展示了享元模式中的主要角色及其交互方式:
传统实现 VS 享元模式
案例需求
案例背景:实现一个文本编辑器的基础字符显示功能,要求支持大量字符(如字母、数字)的显示,每个字符需记录其编码(如’A’、‘1’)和位置(行号、列号)。
传统实现(痛点版)
代码实现:
// 传统实现:每个字符独立创建对象
class Character {private String code; // 字符编码(如'A')private int row; // 行号(外部状态)private int col; // 列号(外部状态)public Character(String code, int row, int col) {this.code = code;this.row = row;this.col = col;}public void display() {System.out.printf("字符:%s,位置:(%d,%d)%n", code, row, col);}
}// 使用示例:创建10000个'A'字符
public class Client {public static void main(String[] args) {List<Character> characters = new ArrayList<>();// 创建10000个'A',每个都是独立对象(内存浪费)for (int i = 0; i < 10000; i++) {characters.add(new Character("A", i / 100, i % 100)); }// 显示所有字符characters.forEach(Character::display);}
}
痛点总结:
- 内存浪费严重:10000个’A’字符被创建为10000个独立对象,但它们的
code
字段完全相同(仅row
和col
不同)。 - 性能低下:大量对象创建/销毁增加GC压力,尤其在高频操作(如文本输入)时可能导致卡顿。
- 管理复杂:无法统一控制字符对象的生命周期(如批量回收或修改共享属性)。
享元模式 实现(优雅版)
代码实现:
// 1. 抽象享元角色:定义字符的公共接口
interface FlyweightCharacter {void display(int row, int col); // 显示方法(接收外部状态:行、列)
}// 2. 具体享元角色:实现字符的共享逻辑(内部状态:code)
class ConcreteCharacter implements FlyweightCharacter {private final String code; // 内部状态(不可变,线程安全)public ConcreteCharacter(String code) {this.code = code;}@Overridepublic void display(int row, int col) {System.out.printf("字符:%s,位置:(%d,%d)%n", code, row, col);}
}// 3. 享元工厂:管理字符对象的共享(单例模式)
class CharacterFactory {private static final CharacterFactory INSTANCE = new CharacterFactory();private final Map<String, FlyweightCharacter> pool = new HashMap<>(); // 缓存池(code→字符对象)private CharacterFactory() {} // 私有构造public static CharacterFactory getInstance() {return INSTANCE;}// 获取或创建字符对象(关键:仅当code不存在时新建)public FlyweightCharacter getCharacter(String code) {if (!pool.containsKey(code)) {pool.put(code, new ConcreteCharacter(code)); // 仅创建一次}return pool.get(code);}// 统计缓存大小(用于验证共享效果)public int poolSize() {return pool.size();}
}// 4. 使用示例:通过工厂获取共享字符对象
public class Client {public static void main(String[] args) {// 创建10000个'A'字符(实际仅创建1个对象)List<FlyweightCharacter> characters = new ArrayList<>();FlyweightCharacter aChar = CharacterFactory.getInstance().getCharacter("A");for (int i = 0; i < 10000; i++) {characters.add(aChar); // 直接复用共享对象}// 显示所有字符(外部状态:行、列动态传入)characters.forEach(charObj -> charObj.display(i / 100, i % 100) // 注意:此处i需替换为实际循环变量);// 验证共享效果:字符池仅1个对象System.out.println("字符池大小:" + CharacterFactory.getInstance().poolSize()); // 输出:1}
}
优势:
- 内存占用骤降:10000个’A’字符仅创建1个
ConcreteCharacter
对象,内存节省99.99%。 - 性能提升:避免了大量对象的创建/销毁开销,GC压力显著降低。
- 易于管理:通过工厂类统一控制字符对象的生命周期(如后续可扩展支持字符样式的批量修改)。
局限:
- 状态分离复杂度:需明确区分内部状态(不可变)和外部状态(动态传入),设计不当可能导致逻辑混乱。
- 线程安全问题:若工厂类非线程安全(如使用普通HashMap),多线程并发获取对象时可能导致数据不一致(可通过
ConcurrentHashMap
解决)。 - 过度共享风险:若对象的外部状态过多或变化频繁,可能导致方法参数膨胀,降低代码可读性。
模式变体
- 线程安全享元工厂:使用
ConcurrentHashMap
作为缓存池,或在工厂方法中添加同步锁,确保多线程环境下对象共享的安全性。 - 带缓存的享元工厂:结合LRU(最近最少使用)淘汰策略,限制缓存池大小,避免内存溢出(适用于对象数量极多的场景)。
- 复合享元模式:将多个简单享元对象组合成复合享元(如“单词”由多个字符组成),支持更复杂的对象复用(如文本编辑器中的单词级共享)。
- 临时享元:支持设置享元对象的过期时间(如缓存中的临时字符样式),自动回收失效对象(结合定时任务或弱引用实现)。
最佳实践
建议 | 理由 |
---|---|
明确区分内部状态与外部状态 | 内部状态(如字符编码)需不可变且与上下文无关,外部状态(如位置)需动态传入,否则无法共享。 |
工厂类使用单例模式 | 确保全局仅一个缓存池实例,避免重复创建导致共享失效。 |
限制外部状态的数量 | 外部状态过多会导致方法参数复杂,降低代码可维护性(建议不超过3个)。 |
对性能敏感场景预加载 | 提前将高频使用的享元对象(如常用字符)加载到工厂池中,避免运行时动态创建。 |
添加监控与日志 | 记录缓存池的命中次数、内存占用等指标,便于优化共享策略(如调整缓存大小)。 |
一句话总结
享元模式通过分离对象的内部状态与外部状态,利用共享机制大幅减少内存占用,是处理大量相似对象场景的高效解决方案。
如果关注Java设计模式内容,可以查阅作者的其他Java设计模式系列文章。😊