享元模式
享元模式
Rif设计模式笔记:享元模式 (Flyweight Pattern)
一、一句话概括
通过共享尽可能多的相似对象来有效地支持大量细粒度对象的复用,从而最大限度地减少内存占用和对象创建的开销。
二、为什么需要它 (Why - The Pain Point)
想象一个场景:你正在开发一个图形编辑器或一个游戏,需要在屏幕上绘制数百万个粒子、树木或字符。
1 | class Particle { |
痛点:内存爆炸和性能瓶燗
如果每个粒子都是一个独立的对象,并且有一百万个粒子,那么你将创建一百万个 Particle 对象。即使 color, sprite, size 这些属性在很多粒子之间都是相同的,它们依然在每个对象中都被存储了一遍。
这会导致:
- 巨大的内存消耗:重复的数据占用了大量的堆内存,可能导致
OutOfMemoryError。 - 性能下降:频繁地创建和销毁大量对象会给 Java 的垃圾回收器(GC)带来巨大压力,导致程序卡顿。
核心问题:当系统中存在大量相似对象,导致内存和性能问题时,如何优化?
三、它是什么 (What - The Solution)
享元模式的解决方案是:将对象的状态进行拆分。
它将一个对象的状态分为两部分:
**内部状态 (Intrinsic State)**:
- 这部分状态是可以被共享的,它不随外部环境的变化而变化。
- 它通常被存储在“享元对象”的内部。
- 在我们的例子中,
color,sprite,size就是内部状态。对于所有红色的、使用同样贴图的粒子,这部分状态是完全相同的。
**外部状态 (Extrinsic State)**:
- 这部分状态是不可以被共享的,它随外部环境的变化而变化,是每个对象特有的。
- 它不能存储在享元对象内部,必须由客户端在每次调用享元对象的方法时,作为参数传入。
- 在我们的例子中,
x,y,velocity就是外部状态,因为每个粒子的位置和速度都是独一无二的。
核心思想和角色:
- **享元接口 (Flyweight)**:
- 定义了享元对象对外提供的接口。
- 它的方法通常需要接收外部状态作为参数。例如,
draw(int x, int y)。
- **具体享元 (Concrete Flyweight)**:
- 实现了享元接口。
- 它包含了内部状态。
- 多个“逻辑上”不同的对象可以共享同一个具体享元实例。
- **享元工厂 (Flyweight Factory)**:
- 这是模式的关键。它负责创建和管理享元对象。
- 它内部通常有一个池(如
HashMap)来缓存已经创建的享元对象。 - 当客户端请求一个享元对象时,工厂会先检查池中是否已存在具有相同内部状态的对象。如果存在,则直接返回该共享实例;如果不存在,则创建一个新的实例,存入池中,然后返回。
- **客户端 (Client)**:
- 客户端不直接创建享元对象,而是通过享元工厂来获取。
- 客户端负责存储和维护所有对象的外部状态。
- 在调用享元对象的方法时,客户端需要将对应的外部状态传递进去。
四、怎么做 (How - The Blueprint)
基本步骤(以绘制不同颜色的圆形为例):
- **定义享元接口
Shape**,包含一个draw(int x, int y)方法,x和y是外部状态。 - **创建具体享元类
Circle**,实现Shape接口。它的内部状态是color。draw方法会使用内部的color和传入的x, y来绘制圆形。 - **创建享元工厂
ShapeFactory**。- 内部维护一个
Map<String, Shape>,key是颜色,value是Circle对象。 - 提供一个
getCircle(String color)方法。此方法检查 Map 中是否已有该颜色的Circle,有则返回,无则创建、放入 Map 再返回。
- 内部维护一个
- 客户端要画100个圆形,它会循环100次。每次循环中:
- 从工厂获取一个特定颜色的
Circle对象(可能是共享的)。 - 计算出这个圆的
x, y坐标(外部状态)。 - 调用
circle.draw(x, y)。
- 从工厂获取一个特定颜色的
Java 示例代码
1 | import java.util.HashMap; |
五、UML 类图
1 | @startuml |
图示解读:
FlyweightPatternDemo(客户端) 不直接new Circle(),而是通过ShapeFactory获取Shape对象。ShapeFactory内部管理着Circle对象的创建和缓存,确保相同内部状态的Circle只被创建一次。
六、优缺点分析
优点 (Pros):
- 极大地减少内存占用:通过共享对象,显著降低了系统中对象的数量,从而节省内存。
- 提升性能:减少了对象的创建和销毁次数,减轻了 GC 的压力,提高了系统性能。
缺点 (Cons):
- 增加了系统复杂性:需要将对象的状态分解为内部和外部状态,这可能会使代码逻辑变得更复杂,不易理解。
- 外部状态的管理:客户端需要自己负责管理所有对象的外部状态,增加了客户端的负担。
- 线程安全问题:享元对象是共享的,如果其内部状态(虽然理论上不应该)被意外修改,需要考虑线程安全问题。
七、在 Java 中的应用
享元模式在 Java 核心库中有非常经典的应用。
**
java.lang.String的字符串常量池 (String Pool)**:- 当你写
String s1 = "abc";和String s2 = "abc";时,JVM 不会创建两个 “abc” 对象。它会检查字符串常量池,发现 “abc” 已经存在,于是让s2和s1指向同一个对象。这里的 “abc” 字符串就是享元对象,它的字符内容是内部状态。
- 当你写
java.lang.Integer.valueOf(int i):- 为了优化,Java 缓存了从 -128 到 127 的
Integer对象。当你调用Integer.valueOf(10)时,它会从一个缓存数组中返回一个预先创建好的Integer对象,而不是new Integer(10)。 Integer i1 = 100;和Integer i2 = 100;,i1 == i2的结果是true。Integer i3 = 200;和Integer i4 = 200;,i3 == i4的结果是false(因为超出了缓存范围)。- 这正是享元模式的应用:
Integer对象是享元,数值是内部状态。
- 为了优化,Java 缓存了从 -128 到 127 的
数据库连接池: 连接池中的每个
Connection对象可以被看作是享元对象。它们被多个线程共享,以避免频繁地创建和销毁昂贵的数据库连接。
总而言之,享元模式是一种以空间换时间、优化性能的强大模式。