在 Java 开发中,字符串操作几乎无处不在。面试时常常会被问到:
-
String、StringBuilder、StringBuffer有什么区别? -
为什么
String是不可变的?
本文带你彻底搞懂这几个类的底层原理和使用场景。
1. 三者的继承关系
先看类图:
java.lang.Object
└── java.lang.String (不可变)
└── java.lang.AbstractStringBuilder
├── java.lang.StringBuilder (可变,非线程安全)
└── java.lang.StringBuffer (可变,线程安全)
结论:
-
String是不可变类,底层用final char[](JDK9 之后是byte[])存储内容。 -
StringBuilder和StringBuffer底层继承自AbstractStringBuilder,本质是一个 可扩容的字符数组。
2. 三者的区别总结
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
| 线程安全 | ✅ 安全(不可变带来的特性) | ❌ 非线程安全 | ✅ 线程安全(方法加 synchronized) |
| 性能 | 低(频繁拼接会产生很多对象) | 高(适合单线程操作字符串) | 较低(比 StringBuilder 慢一点) |
| 底层存储结构 | final char[] / byte[] | 可扩容 char[] / byte[] | 可扩容 char[] / byte[] |
| 适用场景 | 不变字符串,如常量、配置、少量拼接 | 高频拼接、单线程环境 | 高频拼接、多线程环境 |
3. 为什么 String 是不可变的?
3.1 源码分析
String 的核心字段:
public final class String implements java.io.Serializable, Comparable<String> { private final char value[];
private final int hash; // 缓存哈希值 }
-
value被final修饰,指向的数组地址不可变。 -
String类本身是final,不能被继承。 -
修改字符串时,本质上会创建一个新对象。
例如:
String s1 = "hello"; s1 = s1 + " world"; System.out.println(s1);
执行过程:
-
"hello"对象被创建并存储在字符串常量池。 -
" world"对象被创建。 -
s1 + " world"会生成新的字符串"hello world",并返回给s1。
原来的 "hello" 依然存在,并没有被修改。
3.2 不可变的好处
-
安全性
-
String常用作密码、URL、文件路径等,如果可变,容易被篡改。
-
-
线程安全
-
多个线程共享同一个字符串对象时,不需要额外同步。
-
-
提高哈希效率
-
不可变性保证
hashCode一旦计算就不会变化,所以String可以安全地作为HashMap、HashSet的 key。
-
-
字符串常量池优化
-
JVM 可以将相同的字符串对象复用,节省内存。
-
4. 使用场景对比
✅ String
适合少量字符串拼接、固定值:
String name = "Alice"; String msg = "Hello " + name;
✅ StringBuilder
适合单线程环境下的大量拼接:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) { sb.append(i); } System.out.println(sb.toString());
✅ StringBuffer
适合多线程环境下的大量拼接:
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10000; i++) { sb.append(i); } System.out.println(sb.toString());
5. 性能对比测试
public class StringTest {
public static void main(String[] args) {
int count = 100000;
// String
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < count; i++) {
s += i;
}
long end = System.currentTimeMillis();
System.out.println("String耗时: " + (end - start));
// StringBuilder
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuilder耗时: " + (end - start));
// StringBuffer
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < count; i++) {
sbf.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuffer耗时: " + (end - start));
}
}
典型输出结果(机器不同会有差异):
String耗时: 3000ms+
StringBuilder耗时: 20ms
StringBuffer耗时: 30ms
结论:
-
String 性能最差,因为频繁创建新对象。
-
StringBuilder > StringBuffer,单线程情况下推荐使用
StringBuilder。
6. 总结
-
String:不可变,线程安全,适合少量拼接、常量场景。 -
StringBuilder:可变,非线程安全,性能最好,推荐在单线程拼接时使用。 -
StringBuffer:可变,线程安全,适合多线程拼接场景。
面试时答题思路:
先讲三者区别,再说为什么 String 不可变(final 修饰、类是 final、数组引用不可变),最后补充不可变的好处(安全、哈希、常量池)。


被折叠的 条评论
为什么被折叠?



