【设计模式】享元模式(Flyweight)详解:用共享对象对抗内存爆炸

文章目录

    • [1. 引言:当对象多到撑爆内存](#1. 引言:当对象多到撑爆内存)
    • [2. 什么是享元模式](#2. 什么是享元模式)
      • [GoF 定义](#GoF 定义)
    • [3. 享元模式的核心思想](#3. 享元模式的核心思想)
    • [4. 享元模式的结构](#4. 享元模式的结构)
    • [5. 示例:字符系统](#5. 示例:字符系统)
      • [5.1 享元接口](#5.1 享元接口)
      • [5.2 具体享元](#5.2 具体享元)
      • [5.3 享元工厂](#5.3 享元工厂)
      • [5.4 客户端使用](#5.4 客户端使用)
    • [6. 享元模式的优点](#6. 享元模式的优点)
    • [7. 享元模式的缺点](#7. 享元模式的缺点)
    • [8. 享元 vs 对象池](#8. 享元 vs 对象池)
    • [9. JDK 中的享元模式](#9. JDK 中的享元模式)
      • [Integer 缓存](#Integer 缓存)
    • [10. 典型应用场景](#10. 典型应用场景)
    • [11. 一个常见误区](#11. 一个常见误区)
    • 参考

1. 引言:当对象多到撑爆内存

假设你在做一个文字编辑器,需要显示 100 万个字符:

latex 复制代码
Hello World ...

如果每个字符都 new 一个对象:

  • char
  • 字体
  • 颜色
  • 大小

内存会瞬间爆炸。

但实际上:

"a"和"a"是完全可以共享的。

这正是享元模式的出发点。

当对象多到内存扛不住时,就该考虑享元模式。


2. 什么是享元模式

GoF 定义

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

详细解释:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

Flyweight 是"轻量级"的意思,指的是拳击比赛中选手体重的最轻等级。顾名思义,该设计模式的作用是为了让对象更"轻"。

一句话理解:

把可以共享的部分抽出来,让对象变"轻"。


3. 享元模式的核心思想

享元模式的关键是区分两种状态:

状态 含义
内部状态 可以共享,如字符、颜色
外部状态 不可共享,如位置

共享的是内部状态,变化的是外部状态。

  • 内部状态,即不会随着环境的改变而改变的可共享部分。
  • 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

4. 享元模式的结构

包含四个角色:

  1. Flyweight(抽象享元)

通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。

  1. ConcreteFlyweight(具体享元)

它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。

  1. FlyweightFactory(享元工厂)

负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

  1. Unsharable Flyweight(非享元角色)

并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。

  1. Client(客户端)

5. 示例:字符系统

5.1 享元接口

java 复制代码
public interface Character {
    void display(int x, int y);
}

5.2 具体享元

java 复制代码
public class ConcreteCharacter implements Character {

    private char symbol;   // 内部状态

    public ConcreteCharacter(char symbol) {
        this.symbol = symbol;
    }

    @Override
    public void display(int x, int y) {
        System.out.println(symbol + " 显示在 " + x + "," + y);
    }
}

5.3 享元工厂

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class CharacterFactory {

    private static Map<Character, Character> pool = new HashMap<>();

    public static Character get(char c) {
        pool.putIfAbsent(c, new ConcreteCharacter(c));
        return pool.get(c);
    }
}

5.4 客户端使用

java 复制代码
Character c1 = CharacterFactory.get('A');
Character c2 = CharacterFactory.get('A');

System.out.println(c1 == c2); // true

相同字符只创建一次对象。


6. 享元模式的优点

  1. 大幅节省内存
  2. 提高系统性能
  3. 支持海量对象
  4. 对客户端透明

7. 享元模式的缺点

  1. 系统复杂度增加
  2. 需要拆分内外部状态
  3. 调试难度变高

8. 享元 vs 对象池

维度 享元模式 对象池
目标 共享状态 复用对象
是否同时被多个使用
典型 字符、Integer 线程池、连接池

9. JDK 中的享元模式

Integer 缓存

java 复制代码
public class Demo {
    public static void main(String[] args) {
        Integer i1 = 127;
        Integer i2 = 127;

        System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));

        Integer i3 = 128;
        Integer i4 = 128;

        System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
    }
}

结果如下:

JVM 对 -128 ~ 127 进行缓存,典型享元。

可以看到 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。


10. 典型应用场景

  • 字符串常量池
  • 数据库连接参数
  • 游戏地图中的格子
  • 图形系统(颜色、字体)

11. 一个常见误区

享元不是为了"减少 new",而是为了"共享状态"。


参考

享元模式 | 菜鸟教程

《图解设计模式》

享元 - Java教程 - 廖雪峰的官方网站

享元设计模式

相关推荐
风流倜傥唐伯虎11 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码11 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚11 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂11 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang11 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐11 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG11 小时前
JavaTuples 库分析
java
坚持就完事了12 小时前
数据结构之树(Java实现)
java·算法
Monly2112 小时前
Java:修改打包配置文件
java·开发语言
roman_日积跬步-终至千里12 小时前
【架构设计与实现】动态数据源切换:核心代码实现手册
java