
一、介绍
享元模式(FlyWeight),属于结构型设计模式,主要解决实例化大量相同的对象,从而导致可能的内存泄漏的问题。
为了解决这个问题,享元模式提出的解决办法是将相同的对象保存在内存中,且仅保存一个对象,因此该对象应该是不可被修改的,当需要获取该对象实例时,直接从内存中读取即可,从而避免了相同对象的重复创建。
下面是享元模式的定义:
运用共享技术有效地支持大量细粒度的对象
二、主要角色
享元模式有以下四个基本角色:
享元工厂(FlyWeightFactory)
工厂模式的应用,用来创建并管理享元对象,根据某一标准,在完成享元对象实例化以后,判断是否需要对该实例对象进行管理。如果客户端需要从享元工厂中获取某些实例,享元工厂将会判断是否对相同的实例进行管理,如果存在被管理的相同的实例对象,则无需重复对其进行实例化,而是直接返回被管理的对象。
一般来说,享元工厂通过HashMap对需要共享的实例进行管理。
抽象享元(FlyWeight)
享元类的抽象接口,规定了对象的行为。
共享的具体享元(SharedFlyWeightImpl)
实现于抽象享元接口,对其规定的行为进行具体实现。并且该类的实例对象在由享元工厂实例化以后,需要被工厂管理,以便在以后需要相同的对象时直接返回,而不是重复实例化。
非共享的具体享元(UnsharedFlyWeightImpl)
实现于抽象享元接口,对其规定的行为进行具体实现。与共享的具体享元实例不同的是,非共享的具体享元在被享元工厂实例化以后,不被工厂管理,即每一次需要该实例时,都需要享元工厂重复创建

三、基本原理
享元模式的核心思想是分离出不变部分与可变部分,通过共享不变部分来减少对象的创建,从而降低内存消耗和提高性能。这种模式的实现方式是通过将对象的状态分为内部状态和外部状态,内部状态是对象不可变的共享部分,而外部状态是可以改变的独立部分。当多个客户端请求同一个享元对象时,它们实际上共享的是同一个对象,这样就避免了重复创建相同的对象,从而节省了内存空间。同时,享元模式还通过工厂模式来实现对象的创建和管理,通过缓存等技术手段来提高对象的复用效率
四、案例
案例背景:实现一个文本编辑器的基础字符显示功能,要求支持大量字符(如字母、数字)的显示,每个字符需记录其编码(如'A'、'1')和位置(行号、列号)
传统实现(痛点版)
java
public class Test1 {
public static void main(String[] args) {
// 使用示例:创建10000个'A'字符
List<Character> characters = new ArrayList<>();
// 创建10000个'A',每个都是独立对象(内存浪费)
for (int i = 0; i < 10000; i++) {
characters.add(new Character("A", i / 100, i % 100));
}
// 显示所有字符
characters.forEach(Character::display);
}
// 传统实现:每个字符独立创建对象
static class Character {
private String code; // 字符编码(如'A')
private int row; // 行号(外部状态)
private int col; // 列号(外部状态)
public Character(String code, int row, int col) {
this.code = code;
this.row = row;
this.col = col;
}
public void display() {
System.out.printf("字符:%s,位置:(%d,%d)%n", code, row, col);
}
}
}
痛点总结:
- 内存浪费严重:10000个'A'字符被创建为10000个独立对象,但它们的code字段完全相同(仅row和col不同)。
- 性能低下:大量对象创建/销毁增加GC压力,尤其在高频操作(如文本输入)时可能导致卡顿。
- 管理复杂:无法统一控制字符对象的生命周期(如批量回收或修改共享属性)
享元模式 实现(优雅版)
抽象享元角色:定义字符的公共接口
java
public interface FlyweightCharacter {
void display(int row, int col); // 显示方法(接收外部状态:行、列)
}
具体享元角色:实现字符的共享逻辑(内部状态:code)
java
public class ConcreteCharacter implements FlyweightCharacter {
private String code ; // 内部状态
@Override
public void display(int row, int col) {
System.out.printf("字符:%s,位置:(%d,%d)%n", code, row, col);
}
}
享元工厂:管理字符对象的共享(单例模式)
java
public class CharacterFactory {
private static final CharacterFactory INSTANCE = new CharacterFactory();
private final Map<String, FlyweightCharacter> pool = new HashMap<>(); // 缓存池(code→字符对象)
private CharacterFactory() {} // 私有构造
public static CharacterFactory getInstance() {
return INSTANCE;
}
// 获取或创建字符对象(关键:仅当code不存在时新建)
public FlyweightCharacter getCharacter(String code) {
if (!pool.containsKey(code)) {
pool.put(code, new ConcreteCharacter(code)); // 仅创建一次
}
return pool.get(code);
}
// 统计缓存大小(用于验证共享效果)
public int poolSize() {
return pool.size();
}
}
测试

五、优缺点
优点
- 减少内存消耗:通过共享对象,避免创建大量重复的对象实例,从而减少内存消耗。
- 提高性能:由于对象被共享,因此可以减少对象的创建和销毁,从而提高系统的性能。
- 外部状态与内部状态的分离:享元模式将对象的外部状态和内部状态分离,使得对象的状态更加灵活和可维护。
缺点
- 复杂度较高:享元模式需要分离出内部状态和外部状态,这使得程序的逻辑变得复杂,设计不当可能导致逻辑混乱
- 运行时间可能变长:为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
- 需要维护享元池:享元模式需要维护一个享元池来管理共享对象,这可能会引入额外的复杂性
- 线程安全问题 若工厂类非线程安全(如使用普通HashMap),多线程并发获取对象时可能导致数据不一致(可通过
ConcurrentHashMap解决)
六、总结
总之,享元模式适用于那些需要大量使用相同或相似对象,并且需要频繁创建和销毁同一类对象的场景。通过共享对象来避免重复创建相同的对象,从而提高系统性能和降低内存消耗,但是享元模式也存在一些缺点,需要根据具体情况权衡其优缺点,考虑是否适合使用享元模式。