适用读者:写过
String s = ""; for(...) s += x;的人。
前置:S04-B02。
实测环境:OpenJDK 21.0.12.1(2026-11-05)。
覆盖旧文:A-1116(316) A-2409(278) A-1691(78) A-1775(111) A-1769(78) = 861 点。
覆盖扩展:E-S04-009(intern()与常量池)、E-S04-010(UTF-8 与平台编码)。
上一篇:S04-B07 · 异常处理:分类、最佳实践与常见反模式
下一篇:S04-X01 · 八股速查。
1. 一句话结论
String 不可变,所以能被安全共享、能进常量池、能当 HashMap 的 key;代价是"每次修改都要新建对象"——这就是字符串拼接必须用 StringBuilder 的原因。
2. 实测:常量池、intern 与编译期折叠
String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern();
实测输出:
s1==s2: false | s1==s3(intern): true
final 常量拼接 s4==s1: true
非 final 变量拼接 s5==s1: false
| 写法 | 是否与 "hello" 同一对象 |
原因 |
|---|---|---|
"hello" 字面量 |
✅ | 常量池 |
new String("hello") |
❌ | 堆上新对象 |
s2.intern() |
✅ | 返回池中已有对象 |
final String C="hel"; C+"lo" |
✅ | 编译期常量折叠 |
String v="hel"; v+"lo" |
❌ | 运行期 StringBuilder 拼接 |
intern() 的用途(E-S04-009):把字符串放进常量池并复用——适合"大量重复的小字符串"(如状态码、枚举名)。代价是常量池(JDK 7+ 在堆中)会增长,滥用会 OOM。
3. 实测:拼接 20000 次,差 189 倍
String x = "";
for (int i = 0; i < 20000; i++) x += i; // 每次新建一个 String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 20000; i++) sb.append(i); // 复用同一缓冲区
实测:
拼接 20000 次: String+= 1477.6 ms | StringBuilder 7.8 ms
| 方式 | 复杂度 | 说明 |
|---|---|---|
s += x 循环 |
O(n²) | 每次都复制整个字符串 |
StringBuilder.append |
O(n) | 内部 char[] 扩容 |
String.format / +(少量) |
可接受 | 编译器用 invokedynamic 优化(Java 9+) |
注意:单次 + 拼接没问题(编译器会优化成 StringBuilder 或 invokedynamic);循环里的 += 才是灾难。
4. 为什么不可变(三条收益)
| 收益 | 说明 |
|---|---|
| 安全共享 | 多线程共享无需同步 |
| 可做 key | hashCode 可缓存,HashMap 依赖稳定性 |
| 常量池复用 | 相同字面量共用一个对象,省内存 |
代价:每次修改都新建对象 → 用 StringBuilder 解决。
5. 编码:UTF-8 vs 平台默认(E-S04-010)
| 层 | 影响因素 |
|---|---|
| 源文件编码 | javac -encoding(Maven:project.build.sourceEncoding) |
| 运行时字符串 | 内部是 UTF-16(char 是 16 位) |
| 字节转换 | getBytes() 用平台默认编码(不要无参调用) |
| 控制台输出 | 终端编码(Windows 代码页) |
判据:所有 String ↔ byte[] 转换都显式指定编码:
byte[] b = s.getBytes(StandardCharsets.UTF_8); // ✅
byte[] bad = s.getBytes(); // ❌ 依赖平台
(编码排障的完整方法论见 S01-J06。)
6. 五个误区
| 误区 | 真相 |
|---|---|
"== 能比较字符串内容" |
只比引用;内容用 equals |
"intern() 越多越好" |
常量池会增长,滥用 OOM |
"所有 + 拼接都慢" |
单次/常量折叠不慢;循环里才慢 |
"String 是 UTF-8" |
内部 UTF-16;编码只在字节转换时出现 |
"getBytes() 不带参数没事" |
依赖平台默认,跨环境出错 |
7. 自检
new String("x") == "x"为什么 false?intern()之后呢?- 为什么
final常量拼接能"折叠"? - 循环拼接为什么是 O(n²)?实测差多少倍?
- 不可变的三个收益是什么?
- 为什么
getBytes()必须带编码?
8. 下一篇
S04-X01 · 八股速查与跨系列索引
基础层到此结束。下一篇是索引型附录:6 张面试大卡 + 星球总结类卡压成"题 → 一句话答案 → 正文位置",并登记 20 张并发卡的去向(S05)。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-1116/2409/1691/1775/1769;扩展点 E-S04-009/010。常量池与拼接实测为 OpenJDK 21。