01.2 — String 深度解析
定位: Java 中使用频率最高的类 — 不可变性的设计哲学及其在 JVM 中的实现 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 不可变性原理、常量池机制、字符串拼接优化、intern() 行为
一、这是什么?为什么需要它?
是什么
String 是 Java 中表示字符串的类,底层用 final char[] (JDK8) 或 final byte[] (JDK9+) 存储。
java
public final class String
implements Serializable, Comparable<String>, CharSequence {
// JDK8: private final char value[];
// JDK9+: private final byte[] value;
}为什么需要 String 不可变?
如果 String 是可变的,会出什么问题?
场景 1:并发安全
String s = "config";
// 线程 A 读 s,线程 B 同时改 s → 读到一半被修改!
场景 2:常量池复用
String s1 = "hello"; // 常量池
String s2 = "hello"; // 复用 s1 的引用
// 如果 s1 可变,s2 也被改了!
场景 3:HashMap key
Map<String, String> map = new HashMap<>();
map.put("key", "value");
// 如果 String 可变,key 的 hashCode 会变 → map.get("key") 找不到了!所以不可变性的 4 大动机:
| 动机 | 说明 |
|---|---|
| 线程安全 | 不可变对象天然可共享,无需同步 |
| 常量池复用 | 相同字面量的引用安全共享 |
| 哈希值缓存 | String 的 hashCode 只计算一次 |
| 安全性 | ClassLoader 路径等关键场景不会被篡改 |
二、原理拆解
2.1 字符串常量池 (String Pool)
┌─────────────────────────────────────────────────────────────┐
│ String 常量池机制 │
│ │
│ String s = "hello"; │
│ ┌──────────────────┐ │
│ │ String Pool │ │
│ │ (堆中, JDK7+) │ │
│ │ │ │
│ s ─────→│ "hello" │◀──── s2 = "hello" (复用) │
│ │ "world" │ │
│ │ "java" │ │
│ └──────────────────┘ │
│ │
│ String s3 = new String("hello"); │
│ ┌──────────────┐ │
│ s3 ────→│ 堆 (非池) │ │
│ │ "hello" │ ← 和常量池中的 "hello" 不同对象 │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘创建几个对象?
java
String s = "hello"; // 1个(常量池,无则创建)
String s2 = new String("hello"); // 2个(常量池1个 + 堆1个)
// 如果常量池已有 "hello":1个(堆)2.2 String 拼接优化
java
// 方式 1:+ 拼接(编译器优化)
String s = "a" + "b" + "c"; // 编译期直接变成 "abc"
// 方式 2:变量拼接
String a = "a";
String b = "b";
String c = a + b; // 编译器自动用 StringBuilder 优化
// 方式 3:循环拼接
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // ❌ 每次创建 StringBuilder + 新 String
}循环拼接的字节码分析:
s += i 在循环中的真实操作:
① new StringBuilder()
② append(s)
③ append(i)
④ s = toString() // 新 String 对象
1000 次循环 → 1000 个 StringBuilder + 1000 个 String!
→ 应该手动用 StringBuilder!java
// ✅ 正确的循环拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();2.3 intern() 机制
java
String s1 = new String("hello");
String s2 = s1.intern(); // 从常量池获取(或加入)
// intern() 的作用:手动将堆中的 String 放入常量池
// 返回值是常量池中的引用s1 (堆) ──→ "hello" (堆对象)
s2 ──────→ "hello" (常量池中的)
s1 == s2 → false
s1.equals(s2) → true2.4 JDK 9 的 byte[] 优化
JDK 8: char[] value; → 每个 char 2 字节
JDK 9+: byte[] value; → 按编码选择
对于大多数字符串(ISO-8859-1/Latin-1),
从 2字节/字符 → 1字节/字符,内存节省约 50%三、图解全景
┌─────────────────────────────────────────────────────────────┐
│ String、StringBuilder、StringBuffer 对比 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ String │ │
│ │ 不可变 │ │
│ │ final char[] (内容不可改) │ │
│ └─────────────────────────────────┘ │
│ ↑ │
│ (所有字符串操作的基类) │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ │ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ StringBuilder │ │ StringBuffer │ │
│ │ 可变 │ │ 可变 │ │
│ │ 非线程安全 │ │ 线程安全(同步) │ │
│ │ JDK5+ 推荐 │ │ 性能差 │ │
│ └────────────────┘ └──────────────────┘ │
│ │
│ 选择策略: │
│ - 不涉及拼接:String │
│ - 单线程频繁拼接:StringBuilder │
│ - 多线程共享可变字符串:StringBuffer (罕见) │
└─────────────────────────────────────────────────────────────┘四、实战验证
4.1 常量池引用验证
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true (同一常量池引用)
System.out.println(s1 == s3); // false (堆对象 ≠ 常量池)
System.out.println(s1.equals(s3)); // true (内容相等)
// intern
String s4 = s3.intern();
System.out.println(s1 == s4); // true (intern 返回常量池引用)4.2 编译期优化验证
java
String a = "hello";
String b = "world";
// 编译期常量折叠
String c = "hello" + "world"; // 编译期优化为 "helloworld"
String d = "helloworld";
System.out.println(c == d); // true
// 变量拼接(运行时)
String e = a + b; // StringBuilder 运行时拼接
System.out.println(c == e); // false
System.out.println(c.equals(e)); // true4.3 StringBuilder vs 直接拼接性能
java
// 在小规模(<100次)下差异不大
// 在大规模下差异显著
long start = System.nanoTime();
String s = "";
for (int i = 0; i < 10000; i++) {
s += i;
}
long time1 = System.nanoTime() - start;
// 结果:~500ms 或更差(GC 压力大)
start = System.nanoTime();
StringBuilder sb = new StringBuilder(50000);
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
long time2 = System.nanoTime() - start;
// 结果:~2ms五、面试视角
| 追问 | 答案要点 |
|---|---|
| String 不可变到底怎么理解? | final char[] 只保证引用不可变,但不保证数组内容不可变。但 String 类不暴露修改方法,且所有方法返回新 String |
| String s = "a" + "b" + "c" 创建了几个对象? | 1个。编译期常量折叠为 "abc" |
| StringBuilder 和 StringBuffer 的区别? | 前者非线程安全(更快),后者方法加了 synchronized(更慢) |
| String 的 length() 和 char[] length 有什么关系? | length() 返回 value.length;对 Unicode 补充字符 count 不准确——codePointCount() 才准确 |
| JDK 9 String 为什么改为 byte[]? | 大多数 Latin-1 字符只需 1 字节,char[] 方案浪费 50% 空间 |
📚 相关链接
- **数据类型与运算符** — char 类型基础
- **Object类与通用方法** — equals/hashCode 与 String
- ← 返回 **语言基础索引**