一句话总结
String 不可变(final 类 + private final byte[],适合常量/Map key)→ StringBuilder 可变且快(非线程安全,单线程拼接首选)→ StringBuffer 可变且安全(synchronized 方法,多线程场景)。JDK 9 引入 Compact Strings(byte[] + coder),纯 Latin-1 字符内存减半。字符串常量池通过 intern() 复用相同字面量。
初级理解
String 是不可变的:String 类被 final 修饰,底层使用 private final char[](JDK 9 后改为 byte[])存储字符。每次对 String 的修改操作(如拼接、替换)都会创建一个新的 String 对象,原对象不变。
StringBuilder 和 StringBuffer 是可变的:它们都继承自 AbstractStringBuilder,内部使用可变的 char[] 数组,修改操作在原数组上进行,不会创建新对象。
线程安全性:StringBuffer 的方法都加了 synchronized,是线程安全的;StringBuilder 没有同步,线程不安全但性能更高。
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变天然安全) | 不安全 | 安全(synchronized) |
| 性能 | 拼接时最差 | 最高 | 中等 |
| 使用场景 | 少量字符串操作 | 单线程大量拼接 | 多线程大量拼接 |
中级深入
String 的"+"拼接底层原理:Java 编译器会对 String 的"+"操作进行优化。常量拼接在编译期完成,变量拼接则使用 StringBuilder:
循环拼接的性能陷阱:在循环中使用 String 的"+"拼接,每次循环都会创建一个新的 StringBuilder 和 String 对象,导致大量 GC:
StringBuilder 的扩容机制:默认初始容量为 16。当 append 时容量不足,会扩容为 原容量 * 2 + 2,并将原数组复制到新数组。如果知道最终长度,建议在构造时指定容量避免频繁扩容。
高级拓展
字符串常量池(String Pool):JVM 在堆中维护了一个字符串常量池。使用双引号直接创建的字符串会放入常量池,使用 new String() 创建的不会。可以通过 intern() 方法手动将字符串加入常量池。
JDK 9 的 Compact Strings 优化:JDK 9 将 String 底层从 char[] 改为 byte[],并增加了一个 coder 字段标识编码方式(LATIN1 或 UTF16)。如果字符串全是 Latin-1 字符(即每个字符占 1 字节),则使用 LATIN1 编码,内存占用减半。
String 为什么设计为不可变?
1. 字符串常量池的需要:如果 String 可变,常量池中的字符串被修改会影响所有引用它的变量。
2. 安全性:String 广泛用于类名、文件路径、网络连接参数等,不可变性保证了这些关键信息不会被篡改。
3. HashMap 的 key:String 作为最常用的 HashMap key,不可变性保证了 hashCode 不会变化,避免哈希表出现不一致。
4. 线程安全:不可变对象天然线程安全,无需同步。
实战场景
场景:日志拼接性能优化
场景:大量字符串拼接选型
面试模拟
Q:String 为什么设计为不可变?
A:四个原因:1) 字符串常量池:不可变才能安全共享;2) HashMap key:不可变保证 hashCode 稳定;3) 安全性:类名、文件路径等不会被篡改;4) 线程安全:不可变对象天然线程安全。
Q:String 的 "+" 拼接和 StringBuilder 的 append 有什么区别?
A:单次 "+" 拼接编译器会优化为 StringBuilder,但循环中每次 "+" 都会创建新的 StringBuilder 对象,导致大量 GC。循环拼接必须显式使用 StringBuilder 复用同一个对象。另外常量拼接在编译期完成(常量折叠),不产生运行时开销。
Q:intern() 方法的作用是什么?
A:intern() 将字符串放入常量池并返回池中引用。如果池中已有相等字符串,直接返回池中引用;否则将该字符串加入池中。常用于节省内存:大量重复字符串通过 intern() 复用同一对象。但注意常量池在堆中(JDK 7+),过多 intern 也会占用堆内存。