String 的三大设计模式

java-concurrency 📚 learning string-design-patterns · string · flyweight · immutable · proxy · string-constant-pool · intern · stringbuilder · jdk9 · integer-cache

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 返回值、可作方法参数不被外部篡改
可缓存可以安全地共享引用
代价:每次修改都要新建对象,频繁拼字符串要用 StringBuilder。

代理模式(Proxy)

两类代理:
代理用途
StringBuilder/Buffer可变代理——频繁拼字符串用 StringBuilder,最后 toString() 转回不可变 String
JDK 9+ StringLatin1/StringUTF16内部存储代理——通过 coder 字段区分 Latin1(1 字节)和 UTF16(2 字节),Latin1 字符场景内存减半
JDK 8 vs JDK 9+ 存储对比:
版本存储方式每字符字节数
JDK 8char[]固定 2 字节
JDK 9+byte[] + coderLatin1=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的设计,用到了哪些设计模式?》— 2026-09-26 Round 1 Q10, ⭐
  • 关联知识

    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

    ⚡ 对比(Contrast)

    HashMap key— String 的不可变使其成为理想的 HashMap key——hashCode 可缓存、equals 稳定
    🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面