Java设计模式-享元模式

Java设计模式-享元模式

模式概述

享元模式简介

核心思想:通过共享技术高效地支持大量细粒度对象的复用,将对象的公共状态(内部状态)与私有状态(外部状态)分离,仅创建一次公共状态的对象,私有状态在使用时动态传入,从而减少内存占用并提升性能。

模式类型:结构型设计模式(关注对象的组成与复用)。

作用

  • 减少内存消耗:通过共享重复对象,避免大量相似对象的重复创建。
  • 提升系统性能:降低对象创建/销毁的开销,减少垃圾回收压力。
  • 简化对象管理:通过工厂类集中管理共享对象,统一控制对象的生命周期。

典型应用场景

  • 文本编辑器/IDE中的字符对象(如Word中大量重复的字母"a",仅存储一份字符编码,位置、颜色等动态传入)。
  • 游戏开发中的粒子系统(如大量相同属性的子弹、火花,共享基础属性,位置、速度等动态计算)。
  • 数据库连接池(复用已创建的数据库连接,避免频繁创建/关闭连接的开销)。
  • 缓存系统(如Guava Cache,通过共享缓存对象减少重复计算)。

我认为:享元模式是"对象复用"的经典实践,用空间换时间(或用共享换内存),让大量相似对象"共用一张脸,各自有不同的姿态"。

课程目标

  • 理解享元模式的核心思想和经典应用场景
  • 识别应用场景,使用享元模式解决功能要求
  • 了解享元模式的优缺点

核心组件

角色-职责表

角色 职责 示例类名
抽象享元角色(Flyweight) 定义享元对象的公共接口,声明操作外部状态的方法 Character
具体享元角色(ConcreteFlyweight) 实现抽象接口,存储内部状态(不可变),依赖外部状态完成操作 ConcreteCharacter
非共享享元角色(UnsharedFlyweight) 不可共享的享元对象(特殊场景使用,如需要个性化配置的对象) SpecialCharacter
享元工厂(FlyweightFactory) 管理享元对象的创建与共享,确保相同内部状态的对象仅创建一次 CharacterFactory

类图

下面是一个简化的类图表示,展示了享元模式中的主要角色及其交互方式:
实现 创建/管理 <<interface>> FlyweightCharacter +display(int row, int col) ConcreteCharacter -String code // 内部状态(不可变) +ConcreteCharacter(String) +display(int row, int col) CharacterFactory -Map pool // 缓存池(code→字符对象) +getInstance() : CharacterFactory +getCharacter(String code) : FlyweightCharacter +poolSize() : int


传统实现 VS 享元模式

案例需求

案例背景:实现一个文本编辑器的基础字符显示功能,要求支持大量字符(如字母、数字)的显示,每个字符需记录其编码(如'A'、'1')和位置(行号、列号)。

传统实现(痛点版)

代码实现

java 复制代码
// 传统实现:每个字符独立创建对象
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字段完全相同(仅rowcol不同)。
  • 性能低下:大量对象创建/销毁增加GC压力,尤其在高频操作(如文本输入)时可能导致卡顿。
  • 管理复杂:无法统一控制字符对象的生命周期(如批量回收或修改共享属性)。

享元模式 实现(优雅版)

代码实现

java 复制代码
// 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;
    }

    @Override
    public 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设计模式系列文章。😊

相关推荐
老华带你飞7 分钟前
校园交友|基于SprinBoot+vue的校园交友网站(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·校园交友网站
自强的小白44 分钟前
学习Java24天
java·学习
Ashlee_code2 小时前
香港券商櫃台系統跨境金融研究
java·python·科技·金融·架构·系统架构·区块链
还梦呦2 小时前
2025年09月计算机二级Java选择题每日一练——第五期
java·开发语言·计算机二级
2501_924890522 小时前
商超场景徘徊识别误报率↓79%!陌讯多模态时序融合算法落地优化
java·大数据·人工智能·深度学习·算法·目标检测·计算机视觉
從南走到北3 小时前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
qianmoq3 小时前
第04章:数字流专题:IntStream让数学计算更简单
java
带只拖鞋去流浪4 小时前
Java集合(Collection、Map、转换)
java
超级小忍4 小时前
使用 GraalVM Native Image 将 Spring Boot 应用编译为跨平台原生镜像:完整指南
java·spring boot·后端
野犬寒鸦4 小时前
力扣hot100:搜索二维矩阵与在排序数组中查找元素的第一个和最后一个位置(74,34)
java·数据结构·算法·leetcode·list