一、💡 一句话理解
核心结论
String内容不可变,适合保存普通字符串;StringBuilder可变且速度通常更快,适合单线程频繁拼接;StringBuffer可变且方法带同步,适合多个线程共享同一个拼接对象。
记忆:普通字符串用 String,单线程拼接用 Builder,多线程共享拼接才考虑 Buffer。
二、🧭 理论:它们是什么
2.1 核心区别
| 对比项 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 内容能否修改 | 不能,修改会产生新字符串 | 能,在原对象上修改 | 能,在原对象上修改 |
| 线程安全 | 内容不可变,可安全共享 | 不保证线程安全 | 单个方法使用同步保证线程安全 |
| 拼接性能 | 少量拼接够用,循环拼接成本较高 | 通常最快 | 因同步开销,通常比 StringBuilder 慢 |
| 常见场景 | 常量、参数、返回值、少量拼接 | 单线程循环拼接、组装 SQL 或文本 | 多线程确实共享同一个字符缓冲区 |
三、⚙️ 理论:它们怎么工作
3.1 String:内容不可变
String 对象创建后,其内容不能改变。下面的 + 并没有修改原字符串,而是产生了新的字符串对象,再让变量指向新对象。
String text = "Java";
text = text + "面试"; // 得到新的字符串 "Java面试"因此,循环中反复使用 + 可能创建较多临时对象。不过,简单表达式可能被编译器优化,不必把所有 + 都改成 StringBuilder。
3.2 StringBuilder 与 StringBuffer:内容可变
它们内部维护可扩容的字符序列,调用 append() 时可以在同一个对象上继续追加内容。
两者的主要区别是:
StringBuilder不提供同步保证,通常性能更好。StringBuffer的主要操作使用同步,允许多个线程操作同一个实例。
使用边界
StringBuffer只能保证单次方法调用的线程安全。多个调用组合成一个完整业务操作时,仍可能需要额外同步。实际开发中,如果拼接对象只是方法内的局部变量,通常直接使用StringBuilder。
四、🚀 实践:从选择到验证
4.1 可以拿来干什么
- 字符串固定或只做少量拼接:使用
String。 - 在一个线程中循环组装日志、报文或 SQL:使用
StringBuilder。 - 多个线程必须共享并修改同一个字符序列:考虑
StringBuffer,同时检查是否存在组合操作。
4.2 完整实践
文件:StringDemo.java
public class StringDemo {
public static void main(String[] args) {
// String:适合保存最终结果或进行少量拼接。
String title = "Java" + "面试";
// StringBuilder:当前对象只在 main 线程中使用,适合循环追加。
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 3; i++) {
// append:把内容追加到同一个可变字符序列,并返回 builder 自身。
builder.append("第").append(i).append("题");
if (i < 3) {
builder.append("、");
}
}
// toString:把可变字符序列转换成最终的不可变 String。
String result = builder.toString();
System.out.println(title);
System.out.println(result);
}
}运行:
javac StringDemo.java
java StringDemo预期输出:
Java面试
第1题、第2题、第3题五、🎤 面试时怎么流利地说
5.1 回答路线
先说是否可变,再说线程安全和性能,最后给出使用场景。
5.2 30 秒标准回答
String是不可变对象,每次看似修改字符串,实际通常会产生新的字符串,所以适合保存固定内容和少量拼接。StringBuilder和StringBuffer都是可变字符序列,适合频繁追加;区别是StringBuilder不保证线程安全,通常更快,适合单线程或方法内的局部变量;StringBuffer的方法带同步,适合多个线程共享同一个实例。实际开发中一般优先使用String或StringBuilder,只有确实需要共享可变字符序列时才考虑StringBuffer。
5.3 高频追问与短答
- **字符串使用
+一定很慢吗?**不一定。简单拼接可能被编译器优化;需要重点避免的是循环中反复拼接。
可以把“线程安全”拆成两件事理解:对象内部会不会被改坏,以及一连串操作会不会被其他线程插队。
5.3.1 String:内容不会被改坏,但共享变量仍可能出错
String 创建后内容不能修改:
String text = "Java";
text = text + "面试";第二行不是把原来的 "Java" 改成 "Java面试",而是创建一个新字符串,再让变量 text 指向它。因此,多个线程只读取同一个 String 对象时,不会把字符串内容改乱,这种共享是安全的。
但如果多个线程同时修改同一个共享变量:
static String text = "";
// 两个线程都执行这一行。
text = text + "A";这一行可以拆成三个步骤:
- 读取
text的旧值。 - 根据旧值创建一个新字符串。
- 让
text指向新字符串。
假设线程 1 和线程 2 同时读到旧值 "",它们都会生成 "A",然后分别给 text 赋值。两个线程虽然都执行了一次追加,但最终结果可能只是 "A",其中一次修改被覆盖了。
怎么理解
String保证的是“字符串对象的内容不会被修改”,不保证“读取共享变量、拼接、重新赋值”这一整套操作不会被其他线程插队。
5.3.2 StringBuffer:单个方法安全,不代表一连串操作都安全
StringBuffer 的 append()、length()、deleteCharAt() 等方法内部使用了同步。多个线程同时调用其中一个方法时,会排队执行,不会一起修改内部数据。
但是,下面的代码包含两个独立的方法调用:
if (buffer.length() > 0) {
buffer.deleteCharAt(0);
}可能出现下面的执行顺序:
- 线程 1 调用
length(),发现长度大于0。 - 线程 1 还没删除字符,线程 2 先把
buffer清空了。 - 线程 1 继续调用
deleteCharAt(0),此时已经没有字符,可能发生越界异常。
虽然 length() 和 deleteCharAt() 单独调用时都是线程安全的,但两次调用之间仍然可能被其他线程插队。如果业务要求“判断和删除必须连续完成”,就要把整个操作一起加锁:
synchronized (buffer) {
if (buffer.length() > 0) {
buffer.deleteCharAt(0);
}
}怎么理解
StringBuffer保证的是“每个方法单独排队执行”;多个方法组成一个不可分割的业务操作时,仍然需要根据情况在外层统一加锁。
六、📌 总结
String:不可变,适合普通字符串。StringBuilder:可变、无同步,适合单线程频繁拼接。StringBuffer:可变、有同步,适合多线程共享实例。- 日常优先级通常是:
String→StringBuilder→ 确有共享并发需求时使用StringBuffer。