什么是享元模式?应用场景有哪些?
面试考察点
-
核心思想理解:面试官不仅仅是想知道享元模式的定义,更是想看你能否讲清楚 "内部状态" 和 "外部状态" 的区分,这是享元模式的灵魂。
-
JDK 源码关联:能否说出
Integer的IntegerCache、String的常量池、Long的缓存等 JDK 中享元模式的经典应用。面试官最爱问的就是 "Integer 127 和 128 的区别"。 -
性能优化意识:能否从内存优化的角度理解享元模式的价值,以及它在实际项目中如何应用。
核心答案
一句话定义:享元模式通过共享对象来减少内存占用,适用于大量相似对象的场景。它将对象的状态分为 "内部状态"(可共享)和 "外部状态"(不可共享),内部状态相同的对象只保留一份,外部状态由客户端传入。
生活中的例子:去网吧上网。每台电脑都有自己的位置号(外部状态,各不相同),但操作系统都是 Windows 10(内部状态,可以共享)。网吧老板不需要给每台电脑装一套不同的系统,大家共享同一个镜像就行。这样既省硬盘空间,又方便管理。
上面的图展示了享元模式的核心结构:
FlyweightFactory(享元工厂):维护一个享元池(通常是一个Map),客户端请求时先从池中查找,有就直接返回已有对象,没有才创建新对象并放入池中。Flyweight(享元对象):包含内部状态(可共享的部分),通过方法参数接收外部状态(不可共享的部分)。- 内部状态 vs 外部状态:这是享元模式的核心区分。内部状态是对象中不随环境变化的部分,可以共享;外部状态是随环境变化的部分,不能共享,必须由客户端每次传入。
深度解析
一、手写享元模式
用 "围棋" 来举例。围棋只有黑白两种棋子(内部状态),但每个棋子的位置不同(外部状态)。一局棋可能下 200+ 步,如果每步都创建一个新棋子对象,那就创建了 200 多个对象。但用享元模式,只需要 2 个对象——一个黑棋、一个白棋。
// 享元接口
public interface ChessPiece {
void display(int x, int y); // x, y 是外部状态
}
// 具体享元:黑棋
public class BlackPiece implements ChessPiece {
private String color = "黑"; // 内部状态,可共享
@Override
public void display(int x, int y) {
System.out.println(color + "棋 → 位置(" + x + ", " + y + ")");
}
}
// 具体享元:白棋
public class WhitePiece implements ChessPiece {
private String color = "白"; // 内部状态,可共享
@Override
public void display(int x, int y) {
System.out.println(color + "棋 → 位置(" + x + ", " + y + ")");
}
}
享元工厂:
public class ChessFactory {
// 享元池:缓存已创建的对象
private static final Map<String, ChessPiece> pool = new HashMap<>();
// 获取棋子对象:有则返回,无则创建
public static ChessPiece getPiece(String color) {
if (!pool.containsKey(color)) {
System.out.println("创建新棋子:" + color);
if ("黑".equals(color)) {
pool.put(color, new BlackPiece());
} else {
pool.put(color, new WhitePiece());
}
}
return pool.get(color);
}
}
使用:
// 200 步棋,只创建 2 个对象
ChessPiece p1 = ChessFactory.getPiece("黑"); // 创建新棋子:黑
p1.display(3, 4);
ChessPiece p2 = ChessFactory.getPiece("白"); // 创建新棋子:白
p2.display(10, 15);
ChessPiece p3 = ChessFactory.getPiece("黑"); // 复用已有的黑棋对象!
p3.display(5, 7);
ChessPiece p4 = ChessFactory.getPiece("白"); // 复用已有的白棋对象!
p4.display(8, 12);
// 验证:p1 和 p3 是同一个对象
System.out.println(p1 == p3); // true
System.out.println(p2 == p4); // true
输出:
创建新棋子:黑
黑棋 → 位置(3, 4)
创建新棋子:白
白棋 → 位置(10, 15)
黑棋 → 位置(5, 7)
白棋 → 位置(8, 12)
true
true
关键点:黑棋第二次请求时不再创建新对象,而是直接返回享元池中已有的。200 步棋只有 2 个棋子对象在内存里,而不是 200 个。
二、JDK 中的经典应用:Integer 缓存
这是面试中最常考的享元模式应用。先看一道经典面试题:
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
为什么 127 是 true,128 是 false?因为 Integer 内部有一个缓存池 IntegerCache,缓存了 -128 到 127 的 Integer 对象:
// java.lang.Integer.IntegerCache(JDK 源码简化版)
private static class IntegerCache {
static final Integer[] cache = new Integer[256];
static {
// 缓存 -128 ~ 127 的 Integer 对象
for (int i = 0; i < cache.length; i++) {
cache[i] = new Integer(i - 128);
}
}
}
// Integer.valueOf() 方法
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + 128]; // 返回缓存对象
}
return new Integer(i); // 超出范围,创建新对象
}
- -128~127:直接从缓存池取,所有相同值共享同一个对象(
==为true) - 超出范围:每次
new一个新对象(==为false)
这就是典型的享元模式——IntegerCache 就是享元工厂,缓存池就是享元池。内部状态是数值本身,没有外部状态(Integer 是不可变对象)。
类似的缓存还有:
Long:同样缓存 -128~127Short:同样缓存 -128~127Byte:缓存所有值(-128~127)Character:缓存 0~127
三、JDK 中的经典应用:String 常量池
String 的常量池也是享元模式的经典实现。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,共享常量池中的对象
String s3 = new String("hello");
System.out.println(s1 == s3); // false,堆上新建了对象
字面量 "hello" 放在字符串常量池中,相同内容只存一份。通过 new String() 创建的则在堆上新分配内存。JDK 7 之后字符串常量池从永久代移到了堆中,进一步优化了内存管理。
四、线程池也是享元?
严格来说,线程池和数据库连接池并不属于经典的享元模式,但思想相似——都是通过 "池化" 复用对象,避免频繁创建销毁。
区别在于:享元模式关注的是 共享对象的内部状态,减少内存占用;线程池关注的是 复用对象本身,减少创建/销毁的开销。虽然都用了 "池" 这个概念,但解决的问题不同。
五、实际应用场景
| 场景 | 享元对象 | 内部状态 | 外部状态 |
|---|---|---|---|
Integer 缓存 | Integer 对象 | 数值(-128~127) | 无 |
String 常量池 | String 对象 | 字符串内容 | 无 |
| 围棋程序 | 棋子对象 | 颜色(黑/白) | 位置坐标 |
| 文本编辑器 | 字符格式对象 | 字体、字号、颜色 | 字符内容、位置 |
| 游戏中的树 | 树对象 | 树的种类、纹理 | 坐标位置 |
| 地图标记 | 图标对象 | 图标样式 | 经纬度 |
享元模式特别适合这类场景:对象数量庞大,但大部分对象的内部状态是相同的。通过共享内部状态,可以把对象数量从几万降到几十。
面试高频追问
-
Integer a = 127和Integer b = 127为什么a == b是true?- 因为
Integer内部有缓存池(IntegerCache),-128 到 127 之间的值会复用缓存对象,指向同一个引用。超出范围则每次创建新对象。
- 因为
-
享元模式和对象池有什么区别?
- 享元模式关注共享对象的内部状态,目的是减少内存占用。对象池关注复用对象避免频繁创建销毁,目的是提高性能。思想相似,但解决的问题不同。
-
享元模式有什么缺点?
- 增加了系统复杂度,需要区分内部状态和外部状态。如果享元对象很多且不常复用,维护享元池反而浪费内存。另外,外部状态需要客户端每次传入,使用上不如直接持有对象方便。
常见面试变体
- "
Integer的缓存机制是怎样的?" - "
String常量池用到了什么设计模式?" - "如何设计一个节省内存的围棋程序?"
- "享元模式和单例模式有什么区别?"
记忆口诀
核心思想:共享内部状态,外部状态靠传入;用池子缓存对象,能复用就不新建。
JDK 典型应用:Integer 缓存 -128~127,String 常量池存字面量。
内部 vs 外部:内部可共享(颜色、字体),外部各不同(位置、内容)。
总结
享元模式的精髓就是 "共享复用,减少内存"。面试时先说概念和内部/外部状态的区分,然后重点讲 Integer 缓存池和 String 常量池这两个 JDK 经典应用,最后和对象池做区分。能把 Integer 127 和 128 的面试题讲清楚,面试官基本就认可你对享元模式的理解了。
