当前位置: 首页 > news >正文

十二、享元模式

文章目录

  • 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 = "尚未实现,敬请期待!";}}// 按照指定的颜色打印大数字的图像@Overridepublic 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;}@Overridepublic 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 打印不同颜色的 1Digit 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_VALUEh = Math.min(h, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// 如果这个属性不能被解析成一个 int 数,则忽略这个异常}}high = h;// 如果可能的话,从 archive 中加载 IntegerCache.archivedCacheCDS.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.lryc.cn/news/419809.html

相关文章:

  • 黑马Java零基础视频教程精华部分_18_Arrays各种方法
  • RAG私域问答场景超级详细方案(第一期方案)[1]:工业级别构建私域问答(知识处理、知识召回排序、搜索问答模块)
  • 【AI在医疗领域的应用】AI在疾病诊断、个性化治疗等领域的应用
  • SpEL结合AOP示例
  • 【Linux:环境变量】
  • 8月9日笔记
  • API 签名认证:AK(Access Key 访问密钥)和 SK(Secret Key 私密密钥)
  • Redis 单机和集群环境部署教程
  • 华为hcip-big data 学习笔记《一》大数据应用开发总指导
  • 用户画像架构图
  • 37.x86游戏实战-XXX遍历怪物数组
  • go语言中map为什么不会自动初始化?
  • 大数据面试SQL(一):合并日期重叠的活动
  • stm32应用、项目、调试
  • WEB渗透-未授权访问篇
  • x86_64、AArch64、ARM32、LoongArch64、RISC-V
  • git push上不去的问题Iremote reiectedl——文件过大的问题
  • Qt Creator卡顿
  • 数据结构笔记(其五)--串
  • Python爬取高清美女图片
  • gin路由
  • 达梦数据库操作以及报错修改
  • 江科大/江协科技 STM32学习笔记P21
  • 第三方jar自带logback导致本地日志文件不生成
  • 国产数据库备份恢复实现
  • 数据仓库: 2- 数据建模
  • Tomcat 漏洞
  • 分布式消息队列Kafka
  • C# Unity 面向对象补全计划 七大原则 之 迪米特法则(Law Of Demeter )难度:☆☆☆ 总结:直取蜀汉
  • 【C++】—— 类与对象(四)