享元模式:共享实例

Flyweight 是「轻量级」的意思。通过尽量共享实例来避免 new 出实例。
使用场景为需要重用数量有限的同一类对象时。这类对象的特点是:
- 创建和销毁成本较高
- 对象的部分状态可以独立于对象本身存在
- 一般是 站内文章不可变对象。构造函数初始化完成后,不可变对象的状态不会被修改。
应用实例:
- Java 中的
String对象:字符串常量池中已经存在的字符串会被复用。 - 数据库连接池:数据库连接被复用,避免频繁创建和销毁连接。
- Java 线程池的实现
课本案例

BigChar代表重型实例。它将文件中读取大型字符并存储在内存中。BigCharFactory表示生成BigChar的类,生成的示例存储在pool中。BigString由多个BigChar组成

🍐⚱️:其实还可以设计更严谨。让大字符和工厂放同一个包,大字符设
protected
BigCharFactory 中的 getBigChar 方法是享元模式的核心,用 synchronized 修饰。getBigChar 方法的作用是在 pool(HashMap)中寻找 BigChar 实例。很像单例模式中的懒汉模式。
BigCharFactory 使用了单例模式(静态工厂模式)。getInstance 返回的是唯一的工厂对象。
1 | public class BigCharFactory { |

模式解析
享元模式类图:

登场角色:
Flyweight(轻量级):按照通常方式编写程序会导致程序变重,所以如果能够共享实例会比较好,而Flyweight角色表示的就是那些实例会被共享的类。在示例程序中,由BigChar类扮演此角色。FlyweightFactory(轻量级工厂):FlyweightFactory角色是生成Flyweight角色的工厂。在工厂中生成Flyweight角色可以实现共享实例。在示例程序中,由BigCharFactory类扮演此角色。Client(请求者):Client角色使用FlyweightFactory角色来生成Flyweight角色。在示例程序中,由BigString类扮演此角色。
注意本章中的角色划分方法与 GoF 书有些不同。在 GoF 书中,出现了
ConcreteFlyweight角色和UnsharedConcreteFlyweight角色,其中的ConcreteFlyweight角色相当于本书中的Flyweight角色,而UnsharedConcreteFlyweight角色则没有出现在本章的示例程序中。
拓展思路:
- 如果要改变被共享的对象,就会对多个地方产生影响。一个实例的改变会同时反映到所有使用该实例的地方。
- 应当共享的信息:Intrinsic(本质的,固有的)信息,不论实例在哪里、不论在什么情况下都不会改变的信息,不依赖于实例状态的信息。
- 不应当共享的信息:Extrinsic(外在的,非本质的)信息,当实例的位置、状况发生改变时会变化的信息,或是依赖于实例状态的信息。
- 不要让被共享的实例被垃圾回收器回收了。在例子中,pool 字段管理 BigChar 的实例,就不会被看作是垃圾。虽然不能显式地删除实例,但可以删除对实例的引用。要想让实例可以被垃圾回收器回收掉,只需要显式地将其置于管理对象外即可。(从 hashMap 中移除该实例的 Entry)。
| 优点 | 缺点 |
|---|---|
| 如果程序中有很多相似对象,那么该模式可以节省大量内存。 | 可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。 |
| 代码会变得更加复杂。团队中的新成员总是会问:为什么要像这样拆分一个实体的状态? |
模式对比
相关的设计模式:
- 站内文章代理模式:如果生成实例的处理需要花费较长时间,那么使用享元模式可以提高程序的处理速度。而代理模式则是通过设置代理提高程序的处理速度。
- 站内文章组合模式:有时可以使用享元模式共享组合模式中的
Leaf角色。
享元模式和 站内文章单例模式 的区别:
- 单例模式中一个类只能创建一个对象。享元模式更类似与单例模式的变体:多例模式。
- 两者代码近乎相似,但设计意图完全不同。享元模式的目的是对象复用、节省内存;多例模式是为了限制对象的个数。
- 单例对象可变,享元对象不可变。
- 在享元模式的
FlyweightFactory轻量级工厂实现中有时会使用单例模式。
享元模式和缓存的区别:
- 享元模式中,工厂缓存已经创建好的对象,这里的「缓存」是指提前存储的意思。与「数据库缓存」「CPU 缓存」的概念不同,这些平时提到的缓存是用来提高访问效率的,不是为了复用的。
享元模式和对象池的区别:
- 对象池(内存管理)、连接池(数据库连接池)、线程池也是为了复用。池化技术的「复用」理解为「重复使用」,目的为节省对象的创建时间。在任意一个时刻,每一个对象、连接、线程只会被一个使用者独占,使用完毕后会回到池中。
- 享元模式中的复用应理解为「共享使用」。共享示例在整个生命周期中会被多个使用者共享,目的是为了节省时间。
Java 中的享元模式
整数常量池
从 JDK5 开始,对一些不可变类,如 Integer 类做了优化,它具有一个实例缓存(整数常量池),用来存放程序中经常使用的 Integer 实例,这是在类加载的时候一次性创建好的。通过以下方式得到 Integer 对象时会尝试使用缓存:
- 直接自动装箱
Integer.valueOf()
Integer 类中 站内文章静态工厂方法 valueOf(int i) 处理流程如下:
1 | // JDK 源码 |
示例:
1 | public static void main(String[] args) { |
关于基本数据类型 int 和包装类型 Integer 比较的其他规则:
- 由于
Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的 Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true。因为包装类Integer和基本数据类型int比较时,Java 会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较。
字符串常量池
Java 的 String 类也会利用享元模式复用相同的字符串常量。JVM 会专门开辟存储区——字符串常量池存储字符串常量。不过只在字符串常量第一次被用到的时候才会存储到常量池中。
1 | public static void main(String[] args) { |
实际上,享元模式对 JVM 的垃圾回收并不友好,因为享元工厂一致保持对享元对象的引用,这导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。
本文参考
- 《图解设计模式》20 章
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
- 极客时间专栏 - 设计模式之美 - 王争
- 享元模式 | 菜鸟教程
- 享元设计模式
- Java中整数常量池的概念_java小整数池-CSDN博客
- Java基础之int和Integer有什么区别-CSDN博客





