摘要生成中...
AI 摘要
Hunyuan-lite

image.png

Flyweight 是「轻量级」的意思。通过尽量共享实例来避免 new 出实例。

使用场景为需要重用数量有限的同一类对象时。这类对象的特点是:

  • 创建和销毁成本较高
  • 对象的部分状态可以独立于对象本身存在
  • 一般是 站内文章不可变对象。构造函数初始化完成后,不可变对象的状态不会被修改。

应用实例:

  • Java 中的 String 对象:字符串常量池中已经存在的字符串会被复用。
  • 数据库连接池:数据库连接被复用,避免频繁创建和销毁连接。
  • Java 线程池的实现

课本案例

image.png

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

image.png

🍐⚱️:其实还可以设计更严谨。让大字符和工厂放同一个包,大字符设 protected

BigCharFactory 中的 getBigChar 方法是享元模式的核心,用 synchronized 修饰。getBigChar 方法的作用是在 poolHashMap)中寻找 BigChar 实例。很像单例模式中的懒汉模式。

BigCharFactory 使用了单例模式(静态工厂模式)。getInstance 返回的是唯一的工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BigCharFactory {
private HashMap pool = new HashMap();
private static BigCharFactory singleton = new BigCharFactory();
private BigCharFactory(){};// 构造函数
// 单例模式 获取唯一实例
public static BigCharFactory getInstance(){return singleton;}
// 享元模式的核心(类似多例模式)
public synchronized BigChar getBigChar(char charname){
BigChar bc = (BigChar)pool.get(""+charname);
if(bc == null){
bc = new BigChar(charname);
pool.put(""+charname,bc);
}
return bc;
}
}

image.png

模式解析

享元模式类图:

image.png

登场角色:

  • 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
2
3
4
5
6
7
// JDK 源码
public static Integer valueOf(int i) {
// 查缓存,默认缓存 -128 到 127 的整型数
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

示例:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Integer a1 = 1;
Integer a2 = 1; // 自动装箱,Java 会翻译成 Integer.valueOf(1)
Integer a3 = Integer.valueOf(1);
Integer A = new Integer(1);
System.out.println("a1==a2 && a2 == a3, "+(a1==a2 && a2 == a3)); // true
System.out.println("a1 == A, "+(a1==A)); // false
Integer c = 1005;
Integer d = 1005;
System.out.println("c==d, "+(c==d)); // false
}

关于基本数据类型 int 和包装类型 Integer 比较的其他规则:

  • 由于 Integer 变量实际上是对一个 Integer 对象的引用,所以两个通过 new 生成的 Integer 变量永远是不相等的
  • Integer 变量和 int 变量比较时,只要两个变量的值是相等的,则结果为 true。因为包装类 Integer 和基本数据类型 int 比较时,Java 会自动拆包装为 int,然后进行比较,实际上就变为两个 int 变量的比较。

字符串常量池

Java 的 String 类也会利用享元模式复用相同的字符串常量。JVM 会专门开辟存储区——字符串常量池存储字符串常量。不过只在字符串常量第一次被用到的时候才会存储到常量池中。

1
2
3
4
5
6
7
public static void main(String[] args) {
String s1 = "uuanqin.top";
String s2 = "uuanqin.top";
String S = new String("uuanqin.top");
System.out.println(s1==s2); // true
System.out.println(s2==S); // false
}

实际上,享元模式对 JVM 的垃圾回收并不友好,因为享元工厂一致保持对享元对象的引用,这导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。

本文参考