TL;DR(30 秒扫完)
- 享元模式(最核心):字符串常量池,相同字符串只创建一个对象,
"hello" == "hello"为 true - 不可变模式:
public final class+private final byte[] value+ 无 setter + 防御性拷贝 - 代理模式:StringBuilder 是可变代理;JDK 9+
StringLatin1/StringUTF16是内部存储代理 - 不可变四大好处:线程安全、哈希缓存(HashMap key)、安全性、可缓存
- 代价:每次修改要新建对象,频繁拼字符串用 StringBuilder
关键结论
结论 AString 最经典的设计模式是享元模式——字符串常量池让相同字符串共享同一个对象
结论 BString 是不可变对象——
final class + final byte[] value + 无 setter + 所有修改方法返回新对象 + 构造器防御性拷贝结论 CString 的不可变催生了两类代理——StringBuilder/Buffer 作为可变代理,JDK 9+
StringLatin1/StringUTF16 作为内部存储代理结论 D不可变带来四个好处(线程安全、哈希缓存、安全性、可缓存),代价是频繁修改要新建对象
完整讲解(费曼四步)
STEP 1 · 概念
String 的设计主要用到三个设计模式:享元模式(最核心)、不可变模式、代理模式。享元通过字符串常量池让相同字符串共享对象;不可变保证线程安全和哈希稳定;代理让频繁修改场景有可变代理。STEP 2 · 大白话
类比享元模式:想象一个文具店,所有顾客要买"同款中性笔"。文具店不会给每个顾客单独做一支笔(new String),而是有一个"库存区"(常量池),所有同款笔放在一起,顾客直接领走——这就是字符串常量池,"hello" 写多少次都只占一份内存。
>
类比不可变模式:String 就像一张"刻在石头上的字"。你不能在上面改字(无 setter),要改就得刻一块新石头(返回新对象)。好处是石头不会被别人偷偷改(线程安全),坏处是刻石头很贵(频繁修改要新建)。>
类比代理模式:你要写一段很长的话,不可能一块块石头刻。先拿张草稿纸(StringBuilder)随便写,写完了再刻成石头(toString() 转回 String)。这就是 StringBuilder 作为可变代理。
STEP 3 · 底层
享元模式(Flyweight)
核心思想:相同对象共享。 实现机制:字符串常量池(String Constant Pool) 入池规则:| 方式 | 是否入池 |
|---|---|
字面量 String s = "hello" | ✓ 自动入池 |
调用 s.intern() | ✓ 手动入池 |
new String("hello") | ✗ 创建堆对象,不入池 |
String a = "hello";
String b = "hello";
a == b // true(引用相等,共享同一个对象)
String c = new String("hello");
a == c // false(不同对象)
a == c.intern() // true(intern 返回常量池引用)
不可变模式(Immutable Object Pattern)
设计要点:| 要点 | 实现 |
|---|---|
| 类不可继承 | public final class String |
| 字段不可变 | private final byte[] value |
| 无 setter | 所有修改方法返回新对象(concat/substring/toUpperCase) |
| 防御性拷贝 | new String(String original) 会复制底层数组 |
| 好处 | 说明 |
|---|---|
| 线程安全 | 不可变对象天生线程安全,并发场景无需同步 |
| 哈希缓存 | hashCode() 可缓存,作 HashMap 的 key 性能更好 |
| 安全性 | 可作 System.getProperty 返回值、可作方法参数不被外部篡改 |
| 可缓存 | 可以安全地共享引用 |
代理模式(Proxy)
两类代理:| 代理 | 用途 |
|---|---|
| StringBuilder/Buffer | 可变代理——频繁拼字符串用 StringBuilder,最后 toString() 转回不可变 String |
| JDK 9+ StringLatin1/StringUTF16 | 内部存储代理——通过 coder 字段区分 Latin1(1 字节)和 UTF16(2 字节),Latin1 字符场景内存减半 |
| 版本 | 存储方式 | 每字符字节数 |
|---|---|---|
| JDK 8 | char[] | 固定 2 字节 |
| JDK 9+ | byte[] + coder | Latin1=1 字节,UTF16=2 字节 |
常见陷阱
| 陷阱 | 说明 |
|---|---|
== vs equals | 常量池内 == 可能为 true,堆对象 == 为 false |
new String("a") vs "a" | 前者在堆,后者在常量池,不是同一个对象 |
intern() | JDK 7+ 之后常量池移到了堆中 |
new String("a").intern() | 返回常量池引用(如果已有),否则把堆对象加入常量池 |
STEP 4 · 简化
一句话总结:String 是"享元模式 + 不可变模式 + 代理模式"的典型组合——享元节省内存(常量池),不可变保证线程安全和哈希稳定,代理让频繁修改有可变载体。
延伸追问
"a" 和 new String("a") 是同一个对象吗?调 intern() 后呢?字面量在常量池,
new 在堆,引用不同;调 intern() 后如果常量池已有就返回常量池引用,否则把堆对象加入常量池String 不可变有什么好处和代价?
好处——线程安全、可缓存、可作 key;代价——每次修改要新建对象,频繁拼字符串性能差,用 StringBuilder
String 内部 JDK 9+ 为什么改成
byte[] + coder 字段?JDK 8 用
char[](2 字节/字符),JDK 9 用 byte[] + coder 区分 Latin1(1 字节)和 UTF16(2 字节),Latin1 字符场景内存减半速查表
String 三大设计模式:
享元模式(最核心)= 字符串常量池,相同字符串共享
不可变模式 = final class + final 字段 + 无 setter + 防御性拷贝
代理模式 = StringBuilder(可变代理)+ JDK 9+ 内部存储代理
不可变四好处:
线程安全 / 哈希缓存(HashMap key)/ 安全性 / 可缓存
JDK 8 vs JDK 9+:
JDK 8: char[](2 字节/字符)
JDK 9+: byte[] + coder(Latin1=1 字节,UTF16=2 字节)
陷阱:
"hello" == new String("hello") → false
"hello" == "hello".intern() → true
Anki 候选卡片
Q: String 最经典的设计模式是什么?
A: 享元模式——字符串常量池让相同字符串共享同一个对象
Q: String 的不可变模式体现在哪几点?
A: final class + final byte[] value + 无 setter(所有修改返回新对象)+ 构造器防御性拷贝
Q: String 不可变的四大好处?
A: 线程安全、哈希缓存(HashMap key 性能更好)、安全性(可作返回值不被篡改)、可缓存
Q: StringBuilder 和 String 是什么关系?
A: 代理模式——StringBuilder 是 String 的可变代理,频繁修改用 StringBuilder,最后 toString() 转回 String
Q: JDK 9+ String 内部存储为什么改成 byte[] + coder?
A: JDK 8 用 char[](固定 2 字节),JDK 9+ 用 byte[] + coder 区分 Latin1(1 字节)和 UTF16(2 字节),Latin1 场景内存减半
Q: 享元模式的核心思想?内蕴 vs 外蕴状态?
A: 用共享换内存;内蕴状态=可共享的不变量(必须不可变),外蕴状态=调用者自己持有的可变数据
Q: JDK 内置享元完整清单?
A: Integer/Short/Long/Byte 缓存 -128~127、Character 缓存 0~127、Boolean.TRUE/FALSE、String 字符串常量池
关联题目
关联知识
String=享元模式(字符串常量池)+不可变模式(final class+final 字段)+代理模式(StringBuilder+JDK9 内部存储);不可变带来线程安全/哈希缓存/安全性/可缓存四大好处
String 是刻在石头上的字——不能改(不可变),相同字共享同一块石头(享元),改字就刻新石头;要长篇写作就用草稿纸(StringBuilder 代理)
✦ 记 忆 口 诀 ✦
享元=常量池共享;不可变=final class+final 字段;代理=StringBuilder+JDK9 内部存储;JDK 内置享元:Integer/Boolean/String 缓存
关键可视化
String 三大设计模式
flowchart TB S["String"] --> P1["享元模式<br/>Flyweight<br/>最核心"] S --> P2["不可变模式<br/>Immutable"] S --> P3["代理模式<br/>Proxy"] P1 --> A1["字符串常量池<br/>相同字符串共享<br/>'hello' == 'hello' → true"] P2 --> A2["final class<br/>+ final byte[] value<br/>+ 无 setter<br/>+ 防御性拷贝"] P3 --> A3["StringBuilder<br/>可变代理"] P3 --> A4["JDK9+ StringLatin1/UTF16<br/>内部存储代理"]
不可变四大好处
flowchart TB IM["String 不可变"] --> B1["① 线程安全<br/>天生线程安全<br/>并发无需同步"] IM --> B2["② 哈希缓存<br/>hashCode 可缓存<br/>作 HashMap key 性能好"] IM --> B3["③ 安全性<br/>可作 System.getProperty 返回值<br/>可作方法参数不被篡改"] IM --> B4["④ 可缓存<br/>可安全共享引用"] IM -.代价.-> C["频繁修改要新建对象<br/>用 StringBuilder"]
JDK 内置享元完整清单
flowchart TB CACHE["JDK 内置享元"] --> I["Integer / Short / Long / Byte<br/>缓存范围 -128 ~ 127<br/>Integer a=127; Integer b=127;<br/>a == b → true(命中缓存)<br/>Integer a=128; Integer b=128;<br/>a == b → false(超出缓存)"] CACHE --> C["Character<br/>缓存 0 ~ 127"] CACHE --> B["Boolean<br/>只有 TRUE/FALSE<br/>天然享元"] CACHE --> S["String<br/>字符串常量池<br/>相同字符串共享"]
String.intern() 陷阱
flowchart TB INTERN["String.intern() 作用"] --> A["显式把运行期字符串<br/>放入字符串常量池"] A --> BAD["❌ OOM 陷阱<br/>对大量 unique 字符串反复 intern<br/>(UUID / 时间戳 / 参数拼接)<br/>常量池撑爆 → OOM"] A --> GOOD["✅ 正确用法<br/>只对有界字符串做 intern<br/>如 status、type 等枚举值"] INTERN -.内存位置.-> MEM["字符串池位置:<br/>JDK7+ 在 Heap(不是 Metaspace!)<br/>Metaspace 存类元数据"]
享元 vs 单例 vs 原型 vs 对象池
flowchart TB
PAT["四种模式对比"] --> F["享元 Flyweight<br/>共享内蕴不变量<br/>内蕴必须不可变<br/>典型:String、Integer 缓存"]
PAT --> S["单例 Singleton<br/>全局唯一实例<br/>可变<br/>典型:Logger、连接池"]
PAT --> P["原型 Prototype<br/>深拷贝实例<br/>可变<br/>典型:Spring @Scope('prototype')"]
PAT --> PO["对象池 Pool<br/>复用已创建对象<br/>可变<br/>典型:HikariCP、线程池"]常见概念混淆澄清
flowchart TB P1["❌ Spring singleton scope 是享元"] --> A1["✅ Spring singleton 是作用域管理<br/>Bean 携带可变业务状态<br/>不是'内蕴不变量共享'"] P2["❌ 字符串池 = Metaspace"] --> A2["✅ 字符串常量池在 Heap(JDK7+)<br/>Metaspace 存类元数据"] P3["❌ String.intern 移到 Metaspace"] --> A3["✅ JDK7+ 字符串池已移到 Heap<br/>intern 是把引用存入 Heap 的字符串池"]
知识关系
⬆️ 前置(Prerequisite)
concurrency-foundations🔄 延伸(Extends)
hash-map-evolution
🎯 概念
📏 规则
⚠️ 误区
🔍 追问
✨ 口诀
共 0 张卡,点击翻面