---
title: String 的三大设计模式
type: concept
domain: java-concurrency
tags: [string, flyweight, immutable, proxy, string-constant-pool, intern, stringbuilder, jdk9, integer-cache]
status: learning
created: 2026-09-26
last_reviewed: 2026-10-09
method: feynman
related_questions:
  - "String的设计，用到了哪些设计模式？"
  - "什么是享元模式，有哪些具体应用？"
related_knowledge:
  - ./concurrency-foundations.md
  - ./jmm-volatile-cas.md
  - ./hash-map-evolution.md
anki_cards: 8
interview_rounds:
  - "2026-09-26-round-1-Q10"
  - "2026-10-09-round-1-Q10"
---

# String 的三大设计模式

> 享元模式（字符串常量池）+ 不可变模式（final class + final 字段）+ 代理模式（StringBuilder + JDK 9 内部存储）。

## TL;DR（30 秒扫完）

- **享元模式**（最核心）：字符串常量池，相同字符串只创建一个对象，`"hello" == "hello"` 为 true
- **不可变模式**：`public final class` + `private final byte[] value` + 无 setter + 防御性拷贝
- **代理模式**：StringBuilder 是可变代理；JDK 9+ `StringLatin1`/`StringUTF16` 是内部存储代理
- **不可变四大好处**：线程安全、哈希缓存（HashMap key）、安全性、可缓存
- **代价**：每次修改要新建对象，频繁拼字符串用 StringBuilder

## 关键结论

- **结论 A**：String 最经典的设计模式是**享元模式**——字符串常量池让相同字符串共享同一个对象
- **结论 B**：String 是不可变对象——`final class` + `final byte[] value` + 无 setter + 所有修改方法返回新对象 + 构造器防御性拷贝
- **结论 C**：String 的不可变催生了两类代理——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")` | ✗ 创建堆对象，不入池 |

**效果**：
```java
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` 返回值、可作方法参数不被外部篡改 |
| **可缓存** | 可以安全地共享引用 |

**代价**：每次修改都要新建对象，频繁拼字符串要用 StringBuilder。

#### 代理模式（Proxy）

**两类代理**：

| 代理 | 用途 |
|------|------|
| **StringBuilder/Buffer** | 可变代理——频繁拼字符串用 StringBuilder，最后 `toString()` 转回不可变 String |
| **JDK 9+ StringLatin1/StringUTF16** | 内部存储代理——通过 `coder` 字段区分 Latin1（1 字节）和 UTF16（2 字节），Latin1 字符场景内存减半 |

**JDK 8 vs JDK 9+ 存储对比**：
| 版本 | 存储方式 | 每字符字节数 |
|------|---------|------------|
| 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 是"享元模式 + 不可变模式 + 代理模式"的典型组合——享元节省内存（常量池），不可变保证线程安全和哈希稳定，代理让频繁修改有可变载体。

## 享元模式深度补充（2026-10-09 Round 1 Q10）

### 享元模式本质

享元模式是 GoF 创建型设计模式之一，核心是**用共享换内存**——把对象拆成两部分：

| 状态类型 | 含义 | 可变性 | 持有方式 |
|---------|------|-------|---------|
| **内蕴状态（Intrinsic）** | 多个对象共享的不变量 | **必须不可变** | 共享对象内部 |
| **外蕴状态（Extrinsic）** | 每个使用点独有的可变数据 | 可变 | 调用者持有 |

**前提**：内蕴状态必须不可变，否则共享对象之间会相互干扰。

### JDK 内置享元完整清单

| 类型 | 缓存范围 | 示例 | 验证 |
|------|---------|------|------|
| `Integer` | -128 ~ 127 | `Integer a=127; Integer b=127; a==b` | true（命中缓存） |
| `Integer` | 128 及以上 | `Integer a=128; Integer b=128; a==b` | false（超出缓存 new 对象） |
| `Short` | -128 ~ 127 | 同 Integer | 同上 |
| `Long` | -128 ~ 127 | 同 Integer | 同上 |
| `Byte` | -128 ~ 127 | 同 Integer | 同上 |
| `Character` | 0 ~ 127 | `char` 转 `Character` | 同上 |
| `Boolean` | 只有 TRUE/FALSE | 天然享元 | 单例 |
| **`String`** | 字符串常量池 | `"hello"` 全部共享 | 见本文享元模式章节 |

**验证代码**：
```java
Integer a = 127;
Integer b = 127;
System.out.println(a == b);    // true（缓存命中）

Integer c = 128;
Integer d = 128;
System.out.println(c == d);    // false（超出缓存 new 对象）

Integer e = Integer.valueOf(128);
Integer f = Integer.valueOf(128);
System.out.println(e == f);    // false（valueOf 也走缓存逻辑）

Integer g = new Integer(128);
Integer h = new Integer(128);
System.out.println(g == h);    // false（new 永远新对象）
```

### String.intern() 陷阱

**intern() 的作用**：显式把运行期生成的字符串放入常量池。

**OOM 陷阱**：如果循环中对大量 **unique** 字符串（如 UUID、时间戳、请求参数拼接）调用 intern()，常量池会持续增长撑爆内存，引发 `OutOfMemoryError`。

```java
// ❌ 危险代码
while (true) {
    String s = "prefix_" + UUID.randomUUID();
    s.intern();  // 每次都是新字符串，常量池爆炸
}

// ✅ 正确做法：只对有界字符串做 intern
String status = request.getParameter("status").intern();  // status 只有有限取值
```

**典型生产事故**：Spring 早期版本对 Bean Name 做 intern()，恶意请求通过参数拼接 unique 字符串注入，导致 OOM。

### 享元 vs 单例 vs 原型 vs 对象池

| 模式 | 核心思想 | 共享范围 | 可变性 | 典型场景 |
|------|---------|---------|-------|---------|
| **享元（Flyweight）** | 共享内蕴状态 | 内蕴不变量 | 内蕴必须不可变 | String、Integer 缓存、精灵渲染 |
| **单例（Singleton）** | 全局唯一实例 | 整个类 | 可变 | Logger、配置类、连接池 |
| **原型（Prototype）** | 深拷贝实例 | 不共享 | 可变 | Spring @Scope("prototype") |
| **对象池（Pool）** | 复用已创建对象 | 对象生命周期 | 可变 | HikariCP、Tomcat 线程池 |

### 常见概念混淆澄清

| 错误说法 | 正确理解 |
|---------|---------|
| **Spring singleton scope 是享元模式** | 错。Spring singleton 是作用域管理（容器内只创建一次），Bean 携带可变业务状态，不是享元需要的"内蕴不变量共享" |
| **字符串池 = Metaspace** | 错。字符串常量池在 **Heap** 中（JDK7+），Metaspace 存**类元数据**（方法、字段定义） |
| **String.intern() 会把对象移到 Metaspace** | 错。JDK 7+ 字符串池已经移到 Heap，intern() 是把引用存入 Heap 的字符串池 |
| **享元模式必须用 String 实现** | 错。任何满足"内蕴不可变"的对象都可以享元化（数据库行对象模板、游戏精灵等） |

### String 的 5 种设计模式完整清单（更新）

| # | 模式 | 具体体现 |
|---|------|---------|
| 1 | **享元模式**（Flyweight） | 字符串常量池，相同字符串共享 |
| 2 | **不变量模式**（Immutable） | `final class` + `final byte[] value` + 无 setter + 防御性拷贝 |
| 3 | **代理模式**（Proxy） | StringBuilder（可变代理）+ JDK 9+ StringLatin1/StringUTF16（内部存储代理） |
| 4 | **抽象工厂**（Abstract Factory） | JDK 9+ `String` 内部根据字符集选择 `StringLatin1` 或 `StringUTF16` |
| 5 | **策略模式**（Strategy） | `String.compareTo` 支持自然排序 vs 自定义 `Comparator` |

## 延伸追问

1. **`"a"` 和 `new String("a")` 是同一个对象吗？调 `intern()` 后呢？**
   - 字面量在常量池，`new` 在堆，引用不同；调 `intern()` 后如果常量池已有就返回常量池引用，否则把堆对象加入常量池
2. **String 不可变有什么好处和代价？**
   - 好处——线程安全、可缓存、可作 key；代价——每次修改要新建对象，频繁拼字符串性能差，用 StringBuilder
3. **String 内部 JDK 9+ 为什么改成 `byte[]` + coder 字段？**
   - JDK 8 用 `char[]`（2 字节/字符），JDK 9 用 `byte[]` + `coder` 区分 Latin1（1 字节）和 UTF16（2 字节），Latin1 字符场景内存减半

## 速查表（面试前 30 秒扫完）

```
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
```

## 关联题目（题库）

- [ ] 《String的设计，用到了哪些设计模式？》— 2026-09-26 Round 1 Q10, ⭐

## 关联知识

- [并发与并行基础 + 线程安全](./concurrency-foundations.md)
- [JMM 与 volatile/CAS](./jmm-volatile-cas.md)

## Anki 候选卡片

1. **正**：String 最经典的设计模式是什么？**反**：享元模式——字符串常量池让相同字符串共享同一个对象
2. **正**：String 的不可变模式体现在哪几点？**反**：final class + final byte[] value + 无 setter（所有修改返回新对象）+ 构造器防御性拷贝
3. **正**：String 不可变的四大好处？**反**：线程安全、哈希缓存（HashMap key 性能更好）、安全性（可作返回值不被篡改）、可缓存
4. **正**：StringBuilder 和 String 是什么关系？**反**：代理模式——StringBuilder 是 String 的可变代理，频繁修改用 StringBuilder，最后 toString() 转回 String
5. **正**：JDK 9+ String 内部存储为什么改成 byte[] + coder？**反**：JDK 8 用 char[]（固定 2 字节），JDK 9+ 用 byte[] + coder 区分 Latin1（1 字节）和 UTF16（2 字节），Latin1 场景内存减半
6. **正**：享元模式的核心思想？内蕴 vs 外蕴状态？**反**：用共享换内存；内蕴状态=可共享的不变量（必须不可变），外蕴状态=调用者自己持有的可变数据
7. **正**：JDK 内置享元完整清单？**反**：Integer/Short/Long/Byte 缓存 -128~127、Character 缓存 0~127、Boolean.TRUE/FALSE、String 字符串常量池
8. **正**：String.intern() 的 OOM 陷阱？
   **反**：对大量 unique 字符串（UUID/时间戳/参数拼接）反复 intern 会撑爆字符串常量池引发 OOM；生产环境只对有界字符串做 intern

---

*最后更新：2026-10-09（追加享元模式深度补充：本质定义、JDK 内置享元清单、String.intern OOM 陷阱、享元 vs 单例 vs 原型 vs 对象池对比、String 5 种设计模式完整清单）*
