String 类源码精读
概述
java.lang.String 是 Java 中使用最频繁的类之一。它在 JDK 9 中经历了重大重构——从 char[] 改为 byte[] + coder 的 compact strings 方案,大幅减少了内存占用。同时,String 类的不可变性、字符串池、equals()/hashCode() 的缓存机制等,都是理解 Java 对象模型的关键。
本文逐层拆解 String 类的内部存储、核心方法实现以及相关辅助类(StringBuilder、StringJoiner、Pattern)的协作细节。
本文基于 OpenJDK 21 源码分析,关键实现对比 JDK 8 的
char[]方案。
1. String 的内部存储与 compact strings
// java.lang.String(JDK 9+)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value; // 存储字符串内容的字节数组
private final byte coder; // 编码标识:LATIN1=0, UTF16=1
private int hash; // 缓存 hashCode,默认 0
private boolean hashIsZero; // JDK 14+ 区分 hash=0 和未计算
// 省略其他字段和方法...
}1.1 JDK 8 vs JDK 9+ 的存储变迁
| 版本 | 存储方式 | 内存占用(英文) | 内存占用(中文) |
|---|---|---|---|
| JDK 8 | private final char value[] | 每个 char 2 字节 | 每个 char 2 字节 |
| JDK 9+ | private final byte[] value + byte coder | 每个 byte 1 字节(Latin-1) | 每个字符 2 字节(UTF-16) |
JDK 9+ compact strings 的核心改进:
- 如果字符串所有字符都在 Latin-1 范围内(
\u0000~\u00FF),使用LATIN1(coder=0),每个字符 1 字节 - 如果包含任何 Latin-1 范围外的字符,使用
UTF16(coder=1),每个字符 2 字节 - 英文/数字场景内存占用降低 50%
1.2 coder 的自动选择
// java.lang.String 构造器
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
// 从 byte[] 构造时自动检测编码
public String(byte[] bytes, int offset, int length, Charset charset) {
// 委托给 StringCoding.decode()
this.value = StringCoding.decode(charset, bytes, offset, length);
this.coder = LATIN1; // 或 UTF16,取决于解码结果
}coder 字段被 @Stable 注解标记,JIT 编译器可以据此进行激进的优化——一旦该字段被确定为 LATIN1,所有相关的条件分支都可以被优化掉:
// JIT 会将 coder == LATIN1 的分支直接内联消除
byte coder = coder();
if (coder == LATIN1) {
// JIT 编译为直接执行此分支
} else {
// JIT 编译为直接执行此分支
}2. equals() 的逐字节比较
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String aString) {
// 比较之前先检查 coder 是否相同
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}2.1 比较流程
equals("hello") equals("你好")
│ │
├─ this == anObject? → false ├─ this == anObject? → false
├─ anObject instanceof String? → true ├─ anObject instanceof String? → true
├─ coder() == other.coder()? ├─ coder() == other.coder()?
│ │ │ │
│ ├─ LATIN1 == LATIN1 → true │ ├─ UTF16 == UTF16 → true
│ └─ 委托 StringLatin1.equals() │ └─ 委托 StringUTF16.equals()
│ │ │ │
│ ├─ 长度相同? │ ├─ 长度相同?
│ ├─ 逐 byte 比较 │ ├─ 逐 2 byte 比较
│ └─ true/false │ └─ true/false
│ │
│ └─ LATIN1 != UTF16 → false (快速失败)2.2 StringLatin1.equals()
// java.lang.StringLatin1
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
@IntrinsicCandidate注解表示该方法可以被 JIT 替换为 CPU 级别的优化指令,如AVX2向量化比较——一次比较 32 字节。
3. hashCode() 的延迟计算与缓存
// java.lang.String
private int hash; // 默认为 0
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) { // hashIsZero 在 JDK 14+ 引入
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true; // 区分 "" 的 hash=0 和未计算状态
} else {
hash = h; // 缓存计算结果
}
}
return h;
}3.1 哈希算法
// StringLatin1.hashCode()
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff); // 31 = 2^5 - 1,质数且可被 JIT 优化为 (h << 5) - h
}
return h;
}算法公式:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
选择 31 的原因:
- 质数:减少哈希冲突
- 可优化:
31 * h被 JIT 编译为(h << 5) - h,比乘法快
3.2 hashIsZero 的必要性
JDK 14 之前,hash == 0 无法区分是"尚未计算"还是"字符串的哈希值正好为 0"(如空字符串 "")。JDK 14 引入 hashIsZero 字段解决这个歧义:
// 场景:空字符串 ""
String s = "";
int h = s.hashCode(); // h == 0
// JDK 13 及之前:hash 保持 0,下次调用 hashCode() 会重新计算(多余)
// JDK 14+:hashIsZero = true,避免重复计算4. intern() 的字符串池
public native String intern();intern() 是一个 native 方法,它维护一个全局的 StringTable(字符串常量池):
// hotspot/share/classfile/stringTable.cpp
// StringTable 本质上是一个 Hashtable<oop, bool>
// key: 字符串对象的 oop(对象指针)
// value: 占位符
oop StringTable::intern(oop string, TRAPS) {
// 1. 查找 StringTable 中是否已存在相等的字符串
if (StringTable::lookup(string) != NULL) {
return lookup(string); // 返回池中对象
}
// 2. 不存在,将当前字符串加入池中
StringTable::add(string);
return string;
}4.1 StringTable 的演进
| JDK 版本 | 位置 | 实现 | 默认大小 | 备注 |
|---|---|---|---|---|
| JDK 6 | PermGen(永久代) | HashTable | 1009 | PermGen GC 效率低,容易 OOM |
| JDK 7 | Heap(堆) | HashTable | 60013 | 移到堆,享受 Full GC 管理 |
| JDK 8+ | Heap(堆) | Hashtable<oop> | 60013 | 可通过 -XX:StringTableSize=N 调整 |
4.2 代码示例
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1 == s2); // false(堆对象 vs 池对象)
System.out.println(s1 == s2.intern()); // true(s2.intern() 返回池中对象)内存布局:
┌──────────────────┐
│ StringTable │
│ ┌──────────────┐│
│ │ "hello" ││ ←── s1(字面量,编译期入池)
│ │ "world" ││
│ └──────────────┘│
└──────────────────┘
↑ ┌──────────────────┐
│ │ Heap │
└──── intern() ──────│ s2: "hello" │
└──────────────────┘5. substring() 的拷贝策略
// JDK 7+ 实现
public String substring(int beginIndex, int endIndex) {
// ... 参数校验 ...
int subLen = endIndex - beginIndex;
// 每次返回新字符串都拷贝 byte[]
return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
: StringUTF16.newString(value, beginIndex, subLen);
}5.1 JDK 6 vs JDK 7+ 的差异
// JDK 6:共享 char[](通过偏移量和长度)
// 问题:大字符串的 substring 会一直持有原始大数组的引用 → 内存泄漏
String big = new String(new char[1_000_000]);
String small = big.substring(0, 2); // small 仍然引用 1M 的 char[]!
// JDK 7+:拷贝数组
// 优点:substring 独立持有自己的 byte[],不会造成内存泄漏
// 代价:每次 substring 都有 O(n) 的拷贝开销| 版本 | 实现方式 | 性能 | 内存安全 |
|---|---|---|---|
| JDK 6 | 共享 char[] + offset + count | O(1) | 可能内存泄漏 |
| JDK 7+ | 拷贝 byte[] | O(n) | 安全 |
6. StringBuilder.append() 的扩容机制
StringBuilder 继承自 AbstractStringBuilder,其扩容逻辑在父类中实现:
// AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value; // 非 final,可扩容
byte coder; // 编码
int count; // 已使用长度
public AbstractStringBuilder append(String str) {
if (str == null) return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
// 将字符串拷贝到 value 数组中
str.getBytes(value, count, coder);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// 不够 → 扩容
if (minimumCapacity - value.length > 0) {
expandCapacity(minimumCapacity);
}
}
private void expandCapacity(int minimumCapacity) {
// 新容量 = 原容量 * 2 + 2
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minimumCapacity < 0) {
newCapacity = minimumCapacity;
}
// 拷贝到新数组
value = Arrays.copyOf(value, newCapacity);
}
}6.1 扩容策略
初始容量 16(默认)
append("a") ×16 次 → 满
│
▼ 扩容: new = 16 × 2 + 2 = 34
append("b") ×18 次 → 满
│
▼ 扩容: new = 34 × 2 + 2 = 70
append("c") ×36 次 → 满
│
▼ 扩容: new = 70 × 2 + 2 = 142
...每次扩容都涉及 Arrays.copyOf() 全量拷贝,频繁扩容会带来性能开销。
6.2 优化建议
// 如果预知字符串长度,指定初始容量避免多次扩容
StringBuilder sb = new StringBuilder(1024); // 预分配 1024 字节
// 链式 append 比多次拼接更高效(避免创建中间 String 对象)
// 推荐:
sb.append("a").append("b").append("c");
// 不推荐:
String s = "a" + "b" + "c"; // 编译器会优化为 StringBuilder7. String.join() 与 split() 实现
7.1 String.join()
public static String join(CharSequence delimiter, CharSequence... elements) {
// 委托给 StringJoiner
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs : elements) {
joiner.add(cs);
}
return joiner.toString();
}StringJoiner 内部使用 StringBuilder 拼接:
// java.util.StringJoiner
public StringJoiner add(CharSequence newElement) {
if (value == null) {
value = new StringBuilder(); // 懒创建
}
if (prefix != null) {
value.append(prefix);
prefix = null;
}
if (isFirst) {
isFirst = false;
} else {
value.append(delimiter); // 除第一个元素外,每次前加分隔符
}
value.append(newElement);
return this;
}7.2 split()
public String[] split(String regex) {
return split(regex, 0);
}
public String[] split(String regex, int limit) {
// 快速路径:单字符且非特殊正则表达式元字符
char ch = 0;
if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1)
|| (regex.length() == 2
&& regex.charAt(0) == '\\'
&& ((ch = regex.charAt(1)) - '0' | '9' - ch) > 0
&& ((ch - 'a' | 'z' - ch) > 0 && (ch - 'A' | 'Z' - ch) > 0))
&& ch < 128)) {
// 快速路径:按单个字符切分,使用 String.indexOf() 循环查找
return splitWithDelimiter(ch, limit);
}
// 慢速路径:使用 Pattern.compile(regex).split(this, limit)
return Pattern.compile(regex).split(this, limit);
}快速路径优化:对于单字符分隔符(如 ,、|、. 等),直接使用 indexOf() 循环查找,避免了正则表达式的编译开销。
7.3 三种切分方式的性能对比
| 方式 | 实现 | 性能 |
|---|---|---|
split(",") | 快速路径(单字符非元字符) | 快 |
split("\|") | 快速路径(转义单字符) | 快 |
| `split(", | \.")` | 慢速路径(正则) |
split("[,\\.]") | 慢速路径(字符类) | 慢 |
8. String 的不可变性设计
public final class String { ... }8.1 不可变性的 4 层保障
| 层级 | 实现 | 说明 |
|---|---|---|
| 类声明 | final class | 禁止子类继承 |
| 字段 | private final byte[] value | 引用不可变,数组内容也不暴露 |
| 无修改方法 | replace()/toUpperCase() 等返回新对象 | 任何"修改"都返回新 String |
| 防御性拷贝 | getBytes()/toCharArray() 返回拷贝 | 不暴露内部 value 引用 |
8.2 不可变性的好处
// 1. 线程安全——无需同步即可共享
String s = "immutable";
// 多线程同时读取,无需任何锁
// 2. 字符串池安全——多个引用可安全指向同一实例
String s1 = "hello";
String s2 = "hello"; // s1 == s2,指向同一对象
// 3. hashCode 可缓存——值不变,哈希不失效
String key = "key";
int h = key.hashCode(); // 只需计算一次
// 4. 作为 Map 的 key 安全
Map<String, Integer> map = new HashMap<>();
map.put("key", 1); // 值不会在 map 中变化9. 关键类结构一览
java.lang.String (final)
├── byte[] value ← 存储内容(JDK 9+ byte[],JDK 8 char[])
├── byte coder ← LATIN1=0 / UTF16=1(JDK 9+)
├── int hash ← hashCode 缓存
├── boolean hashIsZero ← 区分 hash=0 状态(JDK 14+)
│
├── equals(Object) ← 逐 byte 比较,coder 快速失败
├── hashCode() ← 31 * h + char,延迟计算并缓存
├── intern() ← native → StringTable
├── compareTo(String) ← 逐字符字典序
├── concat(String) ← 新 byte[] 拷贝拼接
├── substring(int,int) ← 新 byte[] 拷贝(JDK 7+)
├── split(String) ← 单字符快速路径 / 正则慢速路径
├── join(CharSequence, ...) ← StringJoiner → StringBuilder
└── getBytes(Charset) ← StringCoding.encode → byte[]辅助类:
java.lang.AbstractStringBuilder
├── java.lang.StringBuilder ← 非线程安全,效率高
└── java.lang.StringBuffer ← synchronized,线程安全
java.util.StringJoiner ← 分隔符拼接,String.join() 底层
java.lang.StringLatin1 ← LATIN1 编码的 equals/hashCode/indexOf
java.lang.StringUTF16 ← UTF16 编码的 equals/hashCode/indexOf
java.lang.StringCoding ← String <-> byte[] 编解码委托10. 性能优化建议
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 大量字符串拼接 | StringBuilder 预分配容量 | 减少扩容拷贝次数 |
| 分割单字符分隔符 | split(",") 直接使用 | 走快速路径,避免正则编译 |
| 字符串去重 | 使用 intern() + -XX:StringTableSize | 共享池中对象,减少内存 |
| 循环内字符串拼接 | 外部创建 StringBuilder | 避免每次循环创建新对象 |
| 子串截取 | JDK 7+ 直接 substring() | 不会造成内存泄漏 |
| 大量相等比较 | 先比较 ==(同一池)再 equals() | 引用比较 O(1),值比较 O(n) |
总结
- compact strings(JDK 9+):
byte[]+coder,英文场景内存降低 50% - equals():先比较
coder快速失败,再逐 byte 比较(可 JIT 向量化) - hashCode():31 乘积累加,延迟计算并缓存(JDK 14+ 区分 hash=0 状态)
- intern():StringTable 哈希表,JDK 7+ 移到堆,
-XX:StringTableSize调优 - substring():JDK 7+ 拷贝数组,避免内存泄漏(以 O(n) 代价换取安全性)
- StringBuilder:1.5 倍 + 2 扩容,预分配容量避免频繁
Arrays.copyOf() - split():单字符分隔符走快速路径(
indexOf循环),正则走慢速路径