【Java八股|第3篇】String、StringBuilder、StringBuffer 区别详解
前言
上一篇我们学习了 String 为什么是不可变的。
既然 String 不可变,那么马上就会引出另一个非常经典的问题:
String、StringBuilder、StringBuffer有什么区别?
这个问题在 Java 面试中非常高频,而且经常和字符串拼接、线程安全、性能优化一起问。
比如面试官可能会问:
String和StringBuilder有什么区别?- 为什么循环拼接字符串不建议用
String? StringBuilder和StringBuffer区别是什么?StringBuffer为什么线程安全?- 实际开发中应该用哪个?
这一篇我们就把这三个类一次讲清楚。
一、先给结论
可以先记住一句话:
String是不可变字符串,每次修改都会产生新对象;StringBuilder是可变字符串,效率高,但线程不安全;StringBuffer也是可变字符串,方法加了同步锁,线程安全,但性能相对低一些。
简单对比:
| 类名 | 是否可变 | 是否线程安全 | 性能 | 常见场景 |
|---|---|---|---|---|
String |
不可变 | 安全 | 频繁拼接时较低 | 少量字符串、常量字符串 |
StringBuilder |
可变 | 不安全 | 高 | 单线程大量字符串拼接 |
StringBuffer |
可变 | 安全 | 较低 | 多线程共享字符串拼接 |
实际开发中,最常用的是:
普通字符串:String
大量拼接:StringBuilder
多线程共享拼接:StringBuffer
不过大多数业务代码里,StringBuilder 会比 StringBuffer 更常见。
二、String:不可变字符串
String 是不可变的。
也就是说,一个 String 对象创建之后,它里面保存的字符串内容不能被修改。
看一个例子:
public class Test {
public static void main(String[] args) {
String str = "hello";
str = str + " world";
System.out.println(str);
}
}
输出结果:
hello world
看起来 str 好像被修改了。
但实际上并不是原来的 "hello" 被修改,而是创建了一个新的字符串对象 "hello world",然后让变量 str 指向新对象。
执行过程可以理解为:
1. 创建字符串对象 "hello"
2. str 指向 "hello"
3. 拼接生成新字符串 "hello world"
4. str 改为指向 "hello world"
5. 原来的 "hello" 没有被修改
所以:
String变量的引用可以改变,但String对象本身的内容不能改变。
三、String 频繁拼接有什么问题?
如果只是少量字符串拼接,使用 String 没什么问题。
比如:
String name = "张三";
String msg = "你好," + name;
这种写法很常见,也很清晰。
但是如果在循环中大量拼接字符串,就不推荐使用 String。
比如:
public class Test {
public static void main(String[] args) {
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
System.out.println(result);
}
}
这段代码的问题在于:
result += i;
每次拼接都可能产生新的字符串对象。
如果循环 10000 次,就会产生很多临时字符串对象,影响性能,也增加垃圾回收压力。
所以在大量拼接场景中,更推荐使用 StringBuilder。
四、StringBuilder:可变字符串
StringBuilder 是可变的。
也就是说,它可以在原有对象的基础上继续追加内容,不需要每次都创建新的字符串对象。
示例:
public class Test {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("hello");
builder.append(" ");
builder.append("world");
System.out.println(builder.toString());
}
}
输出结果:
hello world
这里的:
builder.append("hello");
builder.append(" ");
builder.append("world");
都是在同一个 StringBuilder 对象上进行追加。
最后通过:
builder.toString()
转换成 String。
五、使用 StringBuilder 优化循环拼接
刚才循环拼接的代码,可以改成这样:
public class Test {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append(i);
}
String result = builder.toString();
System.out.println(result);
}
}
这样写的好处是:
- 不会每次循环都创建新的
String对象 - 拼接过程都在
StringBuilder内部完成 - 性能更好
- 内存浪费更少
所以实际开发中,如果遇到循环拼接字符串,优先考虑:
StringBuilder
六、StringBuilder 常用方法
StringBuilder 常见方法如下:
1. append:追加内容
StringBuilder builder = new StringBuilder();
builder.append("Java");
builder.append(" ");
builder.append("后端");
System.out.println(builder);
输出:
Java 后端
append 是最常用的方法。
2. insert:插入内容
StringBuilder builder = new StringBuilder("Java后端");
builder.insert(4, " ");
System.out.println(builder);
输出:
Java 后端
insert(4, " ") 表示在下标 4 的位置插入空格。
3. delete:删除内容
StringBuilder builder = new StringBuilder("Java后端开发");
builder.delete(4, 6);
System.out.println(builder);
输出:
Java开发
注意:
delete(4, 6)
删除的是 [4, 6) 范围内的内容,包含 4,不包含 6。
4. reverse:反转字符串
StringBuilder builder = new StringBuilder("abc");
builder.reverse();
System.out.println(builder);
输出:
cba
这个方法在一些算法题中也经常出现。
5. toString:转换成 String
StringBuilder builder = new StringBuilder();
builder.append("hello");
String str = builder.toString();
很多时候我们用 StringBuilder 完成拼接,最后还是要转换成 String 返回。
七、StringBuffer:线程安全的可变字符串
StringBuffer 和 StringBuilder 很像,都是可变字符串。
示例:
public class Test {
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer();
buffer.append("hello");
buffer.append(" ");
buffer.append("world");
System.out.println(buffer.toString());
}
}
输出:
hello world
从使用方式上看,StringBuffer 和 StringBuilder 很像。
主要区别在于:
StringBuffer是线程安全的,StringBuilder不是线程安全的。
八、StringBuffer 为什么线程安全?
StringBuffer 中很多方法都使用了 synchronized 修饰。
可以简单理解为:
public synchronized StringBuffer append(String str) {
// 追加字符串
}
synchronized 的作用是加锁。
当多个线程同时操作同一个 StringBuffer 对象时,同一时间只能有一个线程进入同步方法执行。
这样可以避免多个线程同时修改内部数据导致数据混乱。
但是加锁也会带来一定性能开销。
所以:
StringBuffer 更安全,但性能相对低一些
StringBuilder 更快,但线程不安全
九、StringBuilder 为什么线程不安全?
StringBuilder 的方法没有使用 synchronized 加锁。
如果多个线程同时操作同一个 StringBuilder,可能会出现数据错乱。
简单示例:
public class Test {
public static void main(String[] args) throws InterruptedException {
StringBuilder builder = new StringBuilder();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
builder.append("a");
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
builder.append("b");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(builder.length());
}
}
理论上长度应该是:
2000
但在并发场景下,StringBuilder 不是线程安全的,结果可能不稳定。
不过在大多数业务代码中,字符串拼接通常发生在一个方法内部,由当前线程独立使用。
这种情况下使用 StringBuilder 就可以了。
十、三者内存和性能对比
1. String
String 不可变。
String s = "a";
s += "b";
s += "c";
每次拼接都可能生成新的字符串对象。
适合:
- 字符串内容固定
- 少量拼接
- 作为常量使用
- 作为 Map 的 key
不适合:
- 大量循环拼接
2. StringBuilder
StringBuilder 可变,不加锁。
适合:
- 单线程字符串拼接
- 方法内部拼接
- 循环拼接
- 日志内容组装
- SQL 字符串组装
3. StringBuffer
StringBuffer 可变,加锁。
适合:
- 多线程共享同一个字符串缓冲对象
- 对线程安全有要求的可变字符串场景
不过现在真实开发中,直接多线程共享一个 StringBuffer 的场景并不算多。
所以多数情况下,我们会优先使用 StringBuilder。
十一、编译器对字符串拼接的优化
有些字符串拼接,Java 编译器会自动优化。
比如:
String s = "hello" + "world";
这两个都是字符串常量,编译器会直接优化成:
String s = "helloworld";
所以这种写法不会产生多余的运行时拼接成本。
再看一个例子:
String name = "张三";
String msg = "hello" + name;
这里 name 是变量,不能在编译期完全确定。
编译器可能会在底层使用类似 StringBuilder 的方式进行拼接。
所以少量普通拼接不用过度担心。
真正需要注意的是循环中的大量拼接:
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
这种场景更应该手动使用 StringBuilder。
十二、面试标准回答
如果面试官问:
String、StringBuilder、StringBuffer有什么区别?
可以这样回答:
String 是不可变字符串,一个 String 对象创建之后内容不能修改。每次对字符串进行拼接或修改,都会生成新的字符串对象,所以不适合大量字符串拼接。
StringBuilder 是可变字符串,它可以在原对象基础上追加、插入、删除内容,不会像 String 那样频繁创建新对象,所以性能更高。但是 StringBuilder 不是线程安全的,适合单线程环境,比如方法内部大量字符串拼接。
StringBuffer 也是可变字符串,和 StringBuilder 用法类似,但是它的方法大多使用了 synchronized 修饰,所以是线程安全的。不过加锁会带来性能开销,因此性能一般比 StringBuilder 低。
实际开发中,普通字符串用 String,单线程大量拼接用 StringBuilder,多线程共享拼接才考虑 StringBuffer。
十三、常见问题总结
1. String 一定比 StringBuilder 慢吗?
不一定。
少量字符串拼接时,String 写法更简单,而且编译器可能会优化。
只有大量拼接,尤其是循环拼接时,StringBuilder 优势才明显。
2. StringBuilder 一定线程不安全吗?
如果多个线程共享同一个 StringBuilder 并同时修改,那么不安全。
如果 StringBuilder 只是方法内部局部变量,每个线程都有自己的对象,一般没有线程安全问题。
3. StringBuffer 一定要用吗?
不一定。
只有在多个线程共享同一个可变字符串对象时,才更适合使用 StringBuffer。
普通业务开发中,StringBuilder 用得更多。
4. StringBuilder 最后为什么要 toString?
因为很多方法返回值、接口响应、日志输出最终需要的是 String。
StringBuilder 只是拼接过程中的工具,最终通常要转换成字符串。
5. SQL 拼接可以用 StringBuilder 吗?
可以,但在 MyBatis 或 JPA 项目中,不建议手动拼接用户输入到 SQL 中,容易有 SQL 注入风险。
如果是动态 SQL,优先使用 MyBatis 的 <if>、<where>、<foreach>,或者使用参数绑定。
十四、实际开发建议
实际开发中可以这样选择:
- 字符串内容固定,用
String - 少量拼接,用
String - 循环中大量拼接,用
StringBuilder - 方法内部临时拼接,用
StringBuilder - 多线程共享同一个拼接对象,用
StringBuffer - 不要为了所谓性能,把所有字符串都改成
StringBuilder - 拼接 SQL 时要注意 SQL 注入问题
- 返回结果时通常还是返回
String
十五、总结
这一篇主要学习了 String、StringBuilder、StringBuffer 的区别。
String 是不可变字符串,适合保存普通字符串和常量字符串。它的内容一旦创建就不能改变,每次修改都会产生新对象。
StringBuilder 是可变字符串,适合单线程环境下大量字符串拼接,性能较高。
StringBuffer 也是可变字符串,但它的方法加了同步锁,线程安全,不过性能相对低一些。
面试时要注意,不要只说“StringBuilder 快,StringBuffer 安全”。更完整的回答应该从是否可变、是否线程安全、性能差异、使用场景这几个角度说明。
下一篇我们继续学习 Java 集合中最经典的面试题:HashMap 底层原理。
更多推荐


所有评论(0)