4. String、StringBuffer、StringBuilder 的区别?为什么 String 设计为不可变?
String、StringBuffer、StringBuilder 的区别,以及 String 为什么设计为不可变
一句话回答
- String:不可变,每次修改都产生新对象,适合常量、少量拼接、作为 key。
- StringBuffer:可变,线程安全,方法加了
synchronized,适合多线程下频繁拼接。 - StringBuilder:可变,线程不安全,但性能最好,适合单线程下频繁拼接。
- String 不可变:为了安全、共享、缓存、线程安全和性能。
1. 三者核心对比
| 维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变天然安全) | 安全,方法 synchronized |
不安全 |
| 性能 | 拼接差,产生大量临时对象 | 中等,锁开销 | 最好 |
| 底层存储 | private final char[](Java 8)/ byte[](Java 9+) |
可扩容 char[] / byte[] |
可扩容 char[] / byte[] |
| 是否可继承 | final class,不可继承 |
final class,不可继承 |
final class,不可继承 |
| 适用场景 | 常量、少量拼接、Map key | 多线程频繁拼接 | 单线程频繁拼接 |
| 出现版本 | 最早 | Java 1.0 | Java 1.5 |
记忆:
String 不可变,StringBuffer 安全但慢,StringBuilder 快但不安全。
单线程用StringBuilder,多线程用StringBuffer,常量用String。
2. String 为什么不可变
2.1 不可变的含义
String 对象一旦创建,其内容就不能被修改。
所有看似“修改”的方法,如 substring、concat、replace、toUpperCase,都是返回新对象,原对象不变。
String s = "hello";
String s2 = s.concat(" world");
System.out.println(s); // hello
System.out.println(s2); // hello world
2.2 如何实现不可变
以 Java 8 为例:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char[] value;
private int hash; // 缓存 hash
}
关键点:
- 类被
final修饰:不能被继承,防止子类破坏不可变性。 - 字符数组
private final:引用不可变,外部无法直接访问。 - 不提供修改内部数组的方法:所有“修改”都返回新对象。
- 构造时拷贝:避免外部数组引用影响内部状态。
hash缓存:因为内容不变,hash 只需计算一次。
Java 9 之后改为
byte[]+coder,为了节省内存(Latin-1 字符只用 1 字节),但不可变性不变。
3. String 设计为不可变的原因
3.1 字符串常量池共享,节省内存
字符串是使用最频繁的对象。JVM 用字符串常量池缓存字面量,多个引用可指向同一对象。
String a = "hello";
String b = "hello";
System.out.println(a == b); // true,指向常量池同一对象
如果 String 可变,修改 a 会影响 b,程序行为不可预期。
不可变才能安全共享。
3.2 线程安全
不可变对象天然线程安全,多线程并发读不需要加锁。
因为状态不可变,不存在竞态条件。
3.3 适合作为 HashMap 的 key
HashMap 依赖 hashCode 和 equals。
如果 String 可变,放入 HashMap 后修改内容,hashCode 变化,就再也找不到这个 key,导致内存泄漏和逻辑错误。
Map<String, Integer> map = new HashMap<>();
String key = "a";
map.put(key, 1);
// 如果 String 可变,修改 key 内容后,map.get(key) 可能找不到
不可变保证 key 的 hash 稳定。
3.4 安全性
字符串常用于类名、文件路径、URL、数据库连接、用户名密码等。
如果可变,可能被恶意篡改:
void connect(String url) {
// 如果 url 在传入后被其他线程修改,可能连接到恶意地址
}
不可变保证传递后内容不会被改变。
3.5 缓存 hash,性能好
String 内部缓存 hash:
private int hash;
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 计算并缓存
hash = h;
}
return h;
}
内容不变,hash 只需算一次,作为 HashMap key 时性能好。
3.6 支持协变与安全发布
不可变对象可以安全地在多线程间发布,不需要额外同步。
这也是函数式编程、并发编程中推荐不可变对象的原因。
4. String 拼接的性能问题
4.1 直接用 + 拼接
String s = "";
for (int i = 0; i < 10000; i++) {
s += i; // 每次循环都创建新 String 和 StringBuilder
}
编译后大致等价于:
for (int i = 0; i < 10000; i++) {
s = new StringBuilder().append(s).append(i).toString();
}
每次循环都创建新 StringBuilder 和新 String,产生大量临时对象,性能差。
注意:如果是编译期常量拼接,如
"a" + "b",编译器会直接优化成"ab",不创建中间对象。
4.2 用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String s = sb.toString();
只创建一个 StringBuilder,最后转一次 String,性能好得多。
4.3 循环内拼接必须用 StringBuilder
// 差
String result = "";
for (String item : list) {
result += item;
}
// 好
StringBuilder sb = new StringBuilder();
for (String item : list) {
sb.append(item);
}
String result = sb.toString();
5. StringBuffer 和 StringBuilder 的实现
两者都继承自 AbstractStringBuilder,核心是一个可扩容的字符数组。
abstract class AbstractStringBuilder {
char[] value;
int count;
public AbstractStringBuilder append(String str) {
// 扩容、拷贝、追加
return this;
}
}
区别在于:
public final class StringBuffer extends AbstractStringBuilder {
@Override
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
}
public final class StringBuilder extends AbstractStringBuilder {
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
StringBuffer方法加synchronized,线程安全,但有锁开销。StringBuilder不加锁,单线程性能更好。
扩容机制
默认容量 16,扩容时通常是 新容量 = 旧容量 * 2 + 2,然后 Arrays.copyOf 拷贝。
如果能预估长度,建议指定初始容量,减少扩容:
StringBuilder sb = new StringBuilder(1024);
6. 如何选择
| 场景 | 推荐 |
|---|---|
| 字符串常量、少量拼接 | String |
| 单线程循环拼接 | StringBuilder |
| 多线程共享拼接 | StringBuffer |
| 作为 Map key | String |
| 需要频繁修改内容 | StringBuilder / StringBuffer |
| 需要线程安全且频繁修改 | StringBuffer |
| 需要不可变、安全传递 | String |
7. 面试口述版
String 是不可变的,每次修改都会产生新对象,适合常量和少量拼接。StringBuffer 和 StringBuilder 是可变的,底层是可变字符数组,适合频繁拼接。StringBuffer 方法加了 synchronized,线程安全但有锁开销;StringBuilder 不加锁,单线程性能最好。所以单线程用 StringBuilder,多线程用 StringBuffer。
String 设计为不可变,主要有几个原因:
第一,字符串常量池需要共享,如果可变,一个引用修改会影响其他引用。
第二,不可变天然线程安全,多线程读不需要加锁。
第三,作为 HashMap 的 key,hash 必须稳定,可变会导致 key 找不到。
第四,安全性,字符串常用于 URL、文件路径、类名等,可变可能被恶意篡改。
第五,可以缓存 hash,提升性能。
实现上,String 被 final 修饰,内部字符数组是 private final,不暴露修改方法,所有修改操作都返回新对象。
8. 常见追问
-
String 真的完全不可变吗?
普通使用下是。通过反射可以修改private final数组内容,但这属于破坏封装,不推荐,且 Java 17+ 模块系统限制更强。 -
String s = "a" + "b"创建几个对象?
编译期优化为"ab",只创建 1 个常量池对象(或复用)。 -
new String("abc")创建几个对象?
最多 2 个:常量池中的"abc"(若不存在则创建),以及堆上的新String对象。 -
String s = new String("abc")和String s = "abc"区别?
前者在堆上创建新对象,后者指向常量池;==比较结果不同。 -
intern()有什么用?
把字符串放入常量池并返回池中引用,可用于节省内存。 -
为什么 Java 9 把
char[]改成byte[]?
节省内存:Latin-1 字符只需 1 字节,char固定 2 字节。通过coder标识编码。 -
StringBuilder 线程不安全会怎样?
多线程并发append可能导致数据丢失、数组越界、内容错乱。 -
字符串拼接什么时候用
+也没问题?
编译期常量拼接、少量拼接、一行内拼接,编译器会优化为StringBuilder。

浙公网安备 33010602011771号