十二、享元模式

news2024/11/12 16:06:38

文章目录

  • 1 基本介绍
  • 2 案例
    • 2.1 Digit 接口
    • 2.2 Color 枚举
    • 2.3 BigDigit 类
    • 2.4 DigitFactory 类
    • 2.5 Client 类
    • 2.6 Client 类的测试结果
    • 2.7 总结
  • 3 各角色之间的关系
    • 3.1 角色
      • 3.1.1 Flyweight ( 抽象享元 )
      • 3.1.2 ConcreteFlyweight ( 具体享元 )
      • 3.1.3 UnsharedFlyweight ( 非享元 )
      • 3.1.4 FlyweightFactory ( 享元工厂 )
      • 3.1.5 Client ( 客户端 )
    • 3.2 类图
  • 4 注意事项
  • 5 在源码中的使用
  • 6 优缺点
  • 7 适用场景
  • 8 总结


1 基本介绍

享元模式(Flyweight Pattern)是一种 结构型 设计模式,其核心思想是通过 共享某些对象复用 它们,和单例模式的单例有相似之处——共享实例对象

此处的翻译并非直译,中文和英文对应的含义如下:

  • 对于中文 享元模式,“享元”实际上是“共享实例对象”的意思。
  • 对于英文 Flyweight,Flyweight 的中文翻译是 蝇量级,这个概念原本是拳击比赛的一种重量级,用在此处表示这种模式可以使程序变得更“轻”,即 占用的内存更少

2 案例

本案例实现了 打印不同颜色的大数字(虽然只有 3 种颜色、3 种大数字)。

2.1 Digit 接口

public interface Digit { // 数字接口
    void print(Color color); // 按照指定颜色打印对象中所存储的数
}

2.2 Color 枚举

public enum Color { // 颜色的枚举
    GREEN("\u001B[32m"), // 绿色
    BLUE("\u001B[34m"), // 蓝色
    PURPLE("\u001B[35m"); // 紫色

    Color(String colorStr) {
        this.colorStr = colorStr;
    }

    private String colorStr; // 保存颜色的字符串

    public String getColorStr() {
        return colorStr;
    }
}

2.3 BigDigit 类

import java.io.PrintStream;

public class BigDigit implements Digit { // 大数字
    private String pattern; // 大数字的图像

    public BigDigit(int num) {
        switch (num) {
            case 1:
                pattern = DIGIT_1_PATTERN;
                break;
            case 2:
                pattern = DIGIT_2_PATTERN;
                break;
            case 3:
                pattern = DIGIT_3_PATTERN;
                break;
            default:
                pattern = "尚未实现,敬请期待!";
        }
    }

    // 按照指定的颜色打印大数字的图像
    @Override
    public void print(Color color) {
        PrintStream defaultPrintStream = System.out; // 保存默认的打印流
        System.setOut(new ColoredDigitPrintStream(color)); // 将打印流更换为指定颜色的打印流

        System.out.println(pattern);

        System.setOut(defaultPrintStream); // 还原默认的打印流
    }

    private static final String DIGIT_1_PATTERN = """
            ................
            ......##........
            ..######........
            ......##........
            ......##........
            ......##........
            ......##........
            ..##########....
            ................"""; // 大数字 1 的图像


    private static final String DIGIT_2_PATTERN = """
            ................
            ....######......
            ..##......##....
            ..........##....
            ......####......
            ....##..........
            ..##............
            ..##########....
            ................"""; // 大数字 2 的图像

    private static final String DIGIT_3_PATTERN = """
            ................
            ....######......
            ..##......##....
            ..........##....
            ......####......
            ..........##....
            ..##......##....
            ....######......
            ................"""; // 大数字 3 的图像

    private static class ColoredDigitPrintStream extends PrintStream { // 带有颜色的数字打印流
        private Color color; // 打印流的颜色

        public ColoredDigitPrintStream(Color color) {
            super(System.out);
            this.color = color;
        }

        @Override
        public void println(String x) {
            super.println(this.color.getColorStr() + x + DEFAULT_COLOR);
        }

        private static final String DEFAULT_COLOR = "\u001B[0m"; // 默认的字符颜色
    }
}

2.4 DigitFactory 类

public class DigitFactory { // 数字的工厂
	// 数字池,存储 int 数 与 数字 的映射
    private static Map<Integer, Digit> digitPool = new HashMap<>();

    /**
     * 获取指定 int 数对应的数字
     * 注意本方法是 synchronized 的,防止多个线程同时创建多个共享实例,类似于单例模式
     *
     * @param num 指定的 int 数
     * @return 返回指定 int 数对应的数字
     */
    public synchronized static Digit getInstance(int num) {
        if (!digitPool.containsKey(num)) { // 如果 digitPool 中不存在 num 对应的数字
            digitPool.put(num, new BigDigit(num)); // 则创建新的大数字
        }
        return digitPool.get(num); // 返回 digitPool 中 num 对应的数字
    }
}

2.5 Client 类

public class Client { // 客户端,测试大数字的输出
    public static void main(String[] args) {
        // 可以通过 digit1 打印不同颜色的 1
        Digit digit1 = DigitFactory.getInstance(1);
        digit1.print(Color.BLUE);
        digit1.print(Color.PURPLE);

        Digit digit2 = DigitFactory.getInstance(2);
        digit2.print(Color.GREEN);

        Digit digit3 = DigitFactory.getInstance(3);
        digit3.print(Color.PURPLE);

        Digit digit3_1 = DigitFactory.getInstance(3);
        System.out.println(digit3 == digit3_1); // true,因为 digit3 和 digit3_1 是同一个对象
    }
}

2.6 Client 类的测试结果

注意:文章中无法显示大数字的颜色,可以自己在 IDEA 上运行一下,即可看到不同的颜色。

................
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
................
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................
................
....######......
..##......##....
..........##....
......####......
..........##....
..##......##....
....######......
................
true

2.7 总结

通过使用享元模式,减少了创建大数字对象的机会,从而降低了程序的内存占用;通过 Map 管理生成的实例,避免了多次 new 新对象耗费的时间。可谓是时间和空间上的双赢。

此外,认真观察大数字的两个“属性”:图像颜色,可以发现:

  • 图像是固定的,不论任何情况都不会改变,所以图像是 应当共享的信息,将其放到 BigDigit 中。
  • 颜色不是固定的,随着 Client 需要的颜色而变化,所以颜色是 不应当共享的信息,将其放到其他类 Color 中。

3 各角色之间的关系

3.1 角色

3.1.1 Flyweight ( 抽象享元 )

该角色负责 将 UnsharedFlyweight 角色作为参数定义 ConcreteFlyweight 角色需要实现的方法。如果系统的功能很简单,则不需要该角色。本案例中,Digit 接口扮演该角色。

3.1.2 ConcreteFlyweight ( 具体享元 )

该角色负责 实现 Flyweight 角色定义的方法定义可以共享的部分。本案例中,BigDigit 类扮演该角色。

3.1.3 UnsharedFlyweight ( 非享元 )

该角色负责 定义无法共享的部分作为参数传入 Flyweight 角色定义的方法中。本案例中,Color 枚举扮演该角色。

3.1.4 FlyweightFactory ( 享元工厂 )

该角色负责 生成可共享的 Flyweight 角色,内部有一个 的字段来保存已生成的 Flyweight 实例,当传入相同的参数时,返回这些已有的实例。要注意 getInstance() 方法需要被 synchronized 关键字修饰,以保证线程安全。本案例中,DigitFactory 类扮演该角色。

3.1.5 Client ( 客户端 )

该角色负责 使用 FlyweightFactory 角色来生成 Flyweight 角色。本案例中,Client 类扮演该角色。

3.2 类图

alt text

说明:FlyweightFactory 角色的 pool 字段 和 getInstance() 方法都是 静态 的,使用了 简单(静态)工厂模式。

4 注意事项

  • 划分外部状态和内部状态:正确划分 内部状态外部状态 是享元模式应用的关键。
    • 内部状态:指对象中 可以共享的信息,这部分信息 存储在享元对象内部不会随环境的改变而改变
    • 外部状态:指对象中 不可共享的信息,这部分信息 在享元对象外部用新的对象来存储随环境改变而改变,在方法调用时 以参数形式传入
  • 生成共享实例的工厂:享元模式中的类通常需要一个 工厂控制对象的 创建 和 管理。当客户对象请求一个享元对象时,享元工厂会 先检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在,则创建一个新的享元对象。
  • 线程安全问题:由于享元模式中的对象可能被多个线程下的客户端 共享,因此必须确保这些对象在并发环境下是线程安全的。这可能需要使用 同步机制保护共享对象的内部状态
  • 性能考虑:虽然享元模式可以显著减少对象的创建和内存使用,但 读取外部状态 可能 会使运行时间稍微变长。在性能敏感的应用中,需要仔细评估这一点。

5 在源码中的使用

在 JDK 中的 Integer 类就使用了享元模式的思想,(如果不设置 VM 参数,则)为常用的 int 数(范围为 [-128, 127])提前初始化其对应的 Integer 对象。

在调用 Integer.valueOf() 时,如果传入的参数在 [-128, 127] 之内,则返回提前初始化的 Integer 对象;否则就为传入的参数初始化一个新的 Integer 对象。

Integer 类的部分代码如下所示:

// java.lang.Integer 类
public final class Integer extends ... implements ... {
    public static Integer valueOf(int i) { // 相当于 享元工厂 的 getInstance()
    	// 如果 i 在范围 [low, high] 内,则直接返回提前初始化好的 Integer 对象
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

	private static class IntegerCache { // Integer 的缓存
        static final int low = -128; // 最小的 int 数
        static final int high; // 最大的 int 数,可能由 VM 参数指定
        static final Integer[] cache; // 保持不变的常量池
        static Integer[] archivedCache; // archive 中的缓存

        static {
            // high 的值可能被 VM 参数所指定
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // 缓存数组的最大长度是 Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // 如果这个属性不能被解析成一个 int 数,则忽略这个异常
                }
            }
            high = h;

            // 如果可能的话,从 archive 中加载 IntegerCache.archivedCache
            CDS.initializeFromArchive(IntegerCache.class); // 此方法是 native 方法
            int size = (high - low) + 1;

            // 如果 archive 缓存 存在 且 足够大,则使用它;否则构建新的缓存
            if (archivedCache == null || size > archivedCache.length) {
                // 将范围 [low, high] 内的 Integer 对象提前放入常量池
                Integer[] c = new Integer[size];
                int j = low;
                for(int i = 0; i < c.length; i++) {
                    c[i] = new Integer(j++);
                }
                archivedCache = c;
            }
            cache = archivedCache;
            // 确保范围 [-128, 127] 内的 Integer 对象提前将其放入常量池
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
}

说明:

  • Integer 类中 写一个静态内部类来创建共享的对象实例,这样可以避免给 valueOf() 方法添加 synchronized 修饰,因为 类的加载是线程安全 的,所以不可能生成两个相同值的 Integer 共享实例。这种思想可以类比到 单例模式静态内部类实现 中。
  • Integer 类中:
    • valueOf() 方法相当于 FlyweightFactory 角色的 getInstance() 方法。
    • IntegerCache.cache 相当于 FlyweightFactory 角色的 pool 字段,用于存储已创建的共享实例。
  • Integer 类并没有完全使用享元模式,它只是提前初始化了部分高频使用的 Integer 对象,将其共享,从而减少 valueOf() 方法创建过多的对象,引发 内存溢出 问题。此外,这么做还可以减少构建共享对象所花费的时间,使 Integer 类更加高效。

6 优缺点

优点

  • 减少内存消耗:享元模式通过 共享对象的内部状态减少内存占用,从而避免为每个相似的对象都创建新的实例。
  • 提高性能:由于 减少了对象的创建和销毁次数,享元模式可以 提高系统的性能。特别是在 频繁创建和销毁对象 的场景中,享元模式能够显著减少这些操作带来的开销。
  • 降低系统复杂性:享元模式将对象的状态分为 内部状态外部状态使得系统更容易理解和维护内部状态享元对象 管理,外部状态客户端 管理,这种分离 降低了系统的复杂性,并有效地 降低了对象间的耦合度

缺点

  • 增加编程复杂性:实现享元模式需要将对象的状态分为 内部状态外部状态,这要求开发者对系统的 状态管理 有深入的理解。同时,区分内部状态和外部状态可能会使代码逻辑变得复杂,增加编程的难度。
  • 可能引入线程安全问题如果多个线程同时访问和修改共享对象,可能会导致线程安全问题。因此,在使用享元模式时,需要特别注意线程安全的处理。
  • 可能增加运行时开销:由于享元对象的 外部状态 是通过参数传递给享元对象的,这可能会增加运行时的开销。特别是在外部状态较多或频繁变化的情况下,这种开销可能会更加明显。

7 适用场景

  • 相似对象的大量使用:当系统中存在大量相似对象,即这些对象只有少部分数据不同时,使用享元模式可以显著减少对象的数量,从而降低内存消耗。
  • 对象具有许多共同属性:当对象具有许多 共同属性,而这些属性可以 被所有实例共享 时,使用享元模式可以避免在每个实例中重复存储这些属性,从而节省内存。
  • 频繁创建和销毁对象:如果系统中频繁地创建和销毁大量对象,这会导致大量的内存分配和回收操作,从而降低系统的性能。使用享元模式可以减少对象的创建和销毁次数,从而提高性能。
  • 需要快速响应时间的系统:在一些需要 快速响应时间 的系统中,如实时游戏、实时交易系统等,减少对象的创建和销毁时间 是非常重要的。享元模式可以通过减少对象的数量来降低这些操作的开销,从而提高系统的响应时间。

8 总结

享元模式 是一种 结构型 设计模式,其核心思想是通过 共享某些对象复用 它们,和单例模式的单例有相似之处,可以减少 内存 和 时间 的消耗。

然而,在使用这种模式时不能随心所欲,共有三点需要特别考虑:

  1. 先明确对象的 外部状态内部状态,然后将 内部状态 放到享元类中,使用享元工厂生产共享实例;将 外部状态 放到另外的类中,在调用共享实例的方法时将其传入。
  2. 由于享元模式可能应用到多线程的环境中,所以在工厂生成共享实例时需要 保证线程安全
  3. 对于一个共享实例的多个引用,如果使用其中一个引用修改这个共享实例,那么会导致所有引用指向的实例都被修改,这也是共享的坏处之一,应该尽可能保证共享实例不会被修改。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1988369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023/8/7 英语每日一段

There is unintended usefulness in this gentle enforcement of empathy. A mere news story makes it easy to deploy the defensive mechanism social scientists call “othering” which dismisses the victim, freak or dupe. But if it’s someone you have watched or …

文件上传绕过最新版安全狗

本文来源无问社区&#xff0c;更多实战内容&#xff0c;渗透思路可前往查看http://www.wwlib.cn/index.php/artread/artid/9960.html http分块传输绕过 http分块传输⼀直是⼀个很经典的绕过⽅式&#xff0c;只是在近⼏年分块传输⼀直被卡的很死&#xff0c;很多waf都开始加 …

数据科学 - 数据可视化(持续更新)

1. 前言​​​​​​​ 数据可视化能够将复杂的数据集转化为易于理解的图形、图表或图像。这种直观的表现形式使得人们能够更快地理解数据的分布、趋势、异常值以及数据之间的关系&#xff0c;从而更深入地洞察数据背后的信息。 数据可视化在数据分析和决策制定过程中具有不可…

【LLM基础知识】LLMs-Attention知识总结笔记v4.0

Attention机制 【1】简要介绍Attention机制 提出Attention的论文**&#xff1a;**Attention Is All You Need 论文地址&#xff1a;https://arxiv.org/pdf/1706.03762.pdf 提出Attention的背景&#xff1a;RNN处理序列数据时&#xff0c;token是逐个喂给模型的。比如在a3的位…

C++:map容器的使用

一、map的使用介绍 map文档介绍 1.1 map的模版参数 Key&#xff1a;键值对中Key的类型 T&#xff1a;键值对中value的类型 Compare&#xff1a;比较器的类型&#xff0c;map中的元素是按照Key来进行比较的&#xff0c;缺省情况&#xff08;不传参数时&#xff09;按照小于来…

健康读物:浮毛带来的危害竟这么大?去浮毛宠物空气净化器分享

前两天去我朋友家玩&#xff0c;进他家扑面而来的浮毛让我觉得呼吸都困难了不少&#xff0c;朋友说也有打扫&#xff0c;空气中的浮毛是真没辙&#xff0c;而且他觉得浮毛那么大又进不了肺部&#xff0c;对健康没啥危害&#xff0c;顶多吃几口猫毛&#xff0c;就没有处理。于是…

2024年7月国产数据库大事记-墨天轮

本文为墨天轮社区整理的2024年7月国产数据库大事件和重要产品发布消息。 目录 2024年7月国产数据库大事记 TOP102024年7月国产数据库大事记&#xff08;时间线&#xff09;产品/版本发布兼容认证代表厂商大事记排行榜新增数据库厂商活动相关资料 2024年7月国产数据库大事记 …

操作系统(七)深入理解Linux内核进程上下文切换

本文深入探讨了Linux内核中的进程上下文切换机制。作者韩传华首先解释了进程上下文的概念&#xff0c;包括虚拟地址空间和硬件上下文&#xff0c;并以Linux 5.0内核源码和ARM64架构为例进行讲解。文章详细介绍了进程上下文切换的两个主要过程&#xff1a;进程地址空间切换和处理…

PHP餐饮点餐系统小程序源码

&#x1f37d;️餐饮新纪元&#xff1a;揭秘高效点餐系统的魅力✨ &#x1f4f1;一键下单&#xff0c;快捷就餐新体验&#x1f680; 在这个快节奏的时代&#xff0c;谁不渴望在忙碌之余享受一顿快速而美味的餐食呢&#xff1f;餐饮点餐系统的出现&#xff0c;就像是为我们的餐…

Spring Boot实战:拦截器

一.拦截器快速入门 1.1了解拦截器 什么是拦截器&#xff1a; 概念 &#xff1a;拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码。 也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也…

CoA:提升大型语言模型的多步推理能力

人工智能咨询培训老师叶梓 转载标明出处 大模型&#xff08;LLMs&#xff09;在处理复杂问题时&#xff0c;往往需要借助外部工具来获取现实世界知识&#xff0c;例如网络搜索、数学和物理规则等。然而现有的工具辅助语言模型在多步推理问题中调用工具时存在效率和准确性的挑战…

企业级敏捷框架:业务驱动型敏捷与产品需求团队

本文介绍了一种新的企业级敏捷框架——业务驱动型敏捷&#xff08;Business-driven Agile&#xff09;与 PRT&#xff08;Product Requirement Team&#xff09;&#xff0c;旨在解决传统敏捷方法中需求定义的瓶颈&#xff0c;从而提升产品价值并提高开发效率。原文: A new ent…

dynamic-datasource+Mybatis多数据源使用

Gitee地址:dynamic-datasource: 基于 SpringBoot 多数据源 动态数据源 主从分离 快速启动器 支持分布式事务 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency&…

腾讯云AI代码助手:智驭Python,编织代码的诗篇 —— 深度测评体验

文章目录 引言&#x1f496;1. 开发环境介绍&#x1f4bb;vscode安装插件方法一&#xff1a;链接访问下载安装方法二&#xff1a;vscode直接安装 2. 使用实例✨1. &#x1f6e1;️代码补全&#xff0c;分秒必争2. &#x1f4a1; 技术对话&#xff0c;智慧碰撞3. &#x1f527; …

【分享】洁净室环境检测必测项目详细解读

环境监测&#xff08;Environmental monitoring&#xff09;在实现此目标中起着重要的作用——它提供了有关制造环境的关键信息&#xff0c;避免放行可能受污染的产品。 由于环境监测在制造过程中的重要性&#xff0c;相关机构围绕市场活动推出了许多法规要求和指南。这些标准随…

el-table自动滚动到最底部

我的需求是这样的&#xff0c;因为我的表格是动态的&#xff0c;可以手动新增行&#xff0c;固定表头&#xff0c;而且需要一屏显示&#xff0c;为了方便用户就需要再新增的时候表格自动向上滚动。 差了官方文档后发现有一个属性可以支持 这个属性正是自己需要的&#xff0c;所…

朵拉朵尚:坚持深耕护肤领域 荣获2023年度影响力品牌奖

朵拉朵尚&#xff1a;坚持深耕护肤领域 荣获2023年度影响力品牌奖 伴随着经济全球化的浪潮&#xff0c;新产业、新业态、新动能不断涌现&#xff0c;我国化妆品消费也迅速崛起&#xff0c;成为近年来化妆品行业发展增长速度最快的国家。1月30日&#xff0c;朵拉朵尚受邀参加快…

使用 Plotly 创建专业可视化时你应该知道的七个关键功能

欢迎来到雲闪世界。我们习惯于在在线报纸上看到交互式可视化&#xff0c;并且我们经常想知道数据记者使用什么工具来创建这些看起来专业的可视化。事实是&#xff0c;创建这种类型的可视化不需要任何特殊软件&#xff1b;Python 中的大多数交互式可视化库都是高度可定制的&…

Stable Diffusion史诗级更新! WebUI 1.10.0时代来了!

前言 大家好&#xff0c;我是每天分享AI应用的萤火君&#xff01; 前几天 AUTOMATIC1111 发布了Stable Diffusion WebUI 1.10&#xff0c;我也在第一时间将云环境的镜像升级到了最新版本&#xff0c;有兴趣的同学可以去体验下&#xff0c;目前已经发布到了AutoDL&#xff0c;…

博客趣二维码生成器网站源码

这款二维码生成源码可以把电子名片、文本、wifi网络、电子邮件、短信、电话号码、网址等信息生成对应的二维码图片。地图位置二维码生成使用是谷歌地图的api地址&#xff0c;懂程序的可以改成国内地图http://www.bokequ.com/588.html