设计模式之享元模式

一、介绍

享元模式(FlyWeight),属于结构型设计模式,主要解决实例化大量相同的对象,从而导致可能的内存泄漏的问题。

为了解决这个问题,享元模式提出的解决办法是将相同的对象保存在内存中,且仅保存一个对象,因此该对象应该是不可被修改的,当需要获取该对象实例时,直接从内存中读取即可,从而避免了相同对象的重复创建。

下面是享元模式的定义:

运用共享技术有效地支持大量细粒度的对象

二、主要角色

享元模式有以下四个基本角色:

享元工厂(FlyWeightFactory)

工厂模式的应用,用来创建并管理享元对象,根据某一标准,在完成享元对象实例化以后,判断是否需要对该实例对象进行管理。如果客户端需要从享元工厂中获取某些实例,享元工厂将会判断是否对相同的实例进行管理,如果存在被管理的相同的实例对象,则无需重复对其进行实例化,而是直接返回被管理的对象。

一般来说,享元工厂通过HashMap对需要共享的实例进行管理。

抽象享元(FlyWeight)

享元类的抽象接口,规定了对象的行为。

共享的具体享元(SharedFlyWeightImpl)

实现于抽象享元接口,对其规定的行为进行具体实现。并且该类的实例对象在由享元工厂实例化以后,需要被工厂管理,以便在以后需要相同的对象时直接返回,而不是重复实例化。

非共享的具体享元(UnsharedFlyWeightImpl)

实现于抽象享元接口,对其规定的行为进行具体实现。与共享的具体享元实例不同的是,非共享的具体享元在被享元工厂实例化以后,不被工厂管理,即每一次需要该实例时,都需要享元工厂重复创建

三、基本原理

享元模式的核心思想是分离出不变部分与可变部分,通过共享不变部分来减少对象的创建,从而降低内存消耗和提高性能。这种模式的实现方式是通过将对象的状态分为内部状态和外部状态,内部状态是对象不可变的共享部分,而外部状态是可以改变的独立部分。当多个客户端请求同一个享元对象时,它们实际上共享的是同一个对象,这样就避免了重复创建相同的对象,从而节省了内存空间。同时,享元模式还通过工厂模式来实现对象的创建和管理,通过缓存等技术手段来提高对象的复用效率

四、案例

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

传统实现(痛点版)

java 复制代码
public class Test1 {
    public static void main(String[] args) {
        // 使用示例:创建10000个'A'字符
        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);

    }


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

享元模式 实现(优雅版)

抽象享元角色:定义字符的公共接口

java 复制代码
public interface FlyweightCharacter {
    void display(int row, int col);  // 显示方法(接收外部状态:行、列)
}

具体享元角色:实现字符的共享逻辑(内部状态:code)

java 复制代码
public class ConcreteCharacter implements FlyweightCharacter {
    private String code ;  // 内部状态
    @Override
    public void display(int row, int col) {
        System.out.printf("字符:%s,位置:(%d,%d)%n", code, row, col);
    }
}

享元工厂:管理字符对象的共享(单例模式)

java 复制代码
public 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();
    }
}

测试

五、优缺点

优点

  • 减少内存消耗:通过共享对象,避免创建大量重复的对象实例,从而减少内存消耗。
  • 提高性能:由于对象被共享,因此可以减少对象的创建和销毁,从而提高系统的性能。
  • 外部状态与内部状态的分离:享元模式将对象的外部状态和内部状态分离,使得对象的状态更加灵活和可维护。

缺点

  • 复杂度较高:享元模式需要分离出内部状态和外部状态,这使得程序的逻辑变得复杂,设计不当可能导致逻辑混乱
  • 运行时间可能变长:为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
  • 需要维护享元池:享元模式需要维护一个享元池来管理共享对象,这可能会引入额外的复杂性
  • 线程安全问题 若工厂类非线程安全(如使用普通HashMap),多线程并发获取对象时可能导致数据不一致(可通过ConcurrentHashMap解决)

六、总结

总之,享元模式适用于那些需要大量使用相同或相似对象,并且需要频繁创建和销毁同一类对象的场景。通过共享对象来避免重复创建相同的对象,从而提高系统性能和降低内存消耗,但是享元模式也存在一些缺点,需要根据具体情况权衡其优缺点,考虑是否适合使用享元模式。

相关推荐
Derek_Smart2 小时前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot
NE_STOP3 小时前
MyBatis-mybatis入门与增删改查
java
孟陬6 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
想用offer打牌6 小时前
一站式了解四种限流算法
java·后端·go
华仔啊7 小时前
Java 开发千万别给布尔变量加 is 前缀!很容易背锅
java
也些宝8 小时前
Java单例模式:饿汉、懒汉、DCL三种实现及最佳实践
java
阿星AI工作室8 小时前
给openclaw龙虾造了间像素办公室!实时看它写代码、摸鱼、修bug、写日报,太可爱了吧!
前端·人工智能·设计模式
Nyarlathotep01138 小时前
SpringBoot Starter的用法以及原理
java·spring boot
wuwen58 小时前
WebFlux + Lettuce Reactive 中 SkyWalking 链路上下文丢失的修复实践
java