Java 字符串: String, StringBuffer, StringBuilder 详解
本文对比了String、StringBuffer和StringBuilder:String不可变;后两者可变,核心区别在于线程安全与性能。
这是一个在Java面试和日常开发中都非常重要的问题。我们来详细地解析一下String, StringBuffer, 和 StringBuilder 的区别。
一句话总结
String:不可变的字符序列。StringBuffer:可变的字符序列,线程安全,但效率较低。StringBuilder:可变的字符序列,线程不安全,但效率最高。
核心区别对比
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 (Mutability) | 不可变 (Immutable) | 可变 (Mutable) | 可变 (Mutable) |
| 线程安全性 (Thread Safety) | 线程安全 | 线程安全 (Synchronized) | 线程不安全 |
| 性能 (Performance) | 在频繁修改时性能最低 | 在频繁修改时性能中等 | 在频繁修改时性能最高 |
| 底层实现 | private final char[] value |
char[] value |
char[] value |
| 适用场景 | 少量、基本不改变的字符串 | 多线程环境下共享、频繁修改的字符串 | 单线程环境下、频繁修改的字符串 |
深入解析
1. String (不可变性)
String 对象一旦被创建,其内容就不能被修改。它的所有看似修改的操作(如 +、concat、substring 等)实际上都是创建了一个新的 String 对象,而原始对象保持不变。
为什么不可变?
查看String的源码,你会发现它的核心是一个被 final 修饰的字符数组:
private final char[] value;
final 关键字意味着 value 数组的引用地址不可改变,并且Java的实现保证了数组内容也不会被外部方法修改。
不可变性的优缺点:
- 优点:
- 线程安全:因为无法修改,所以可以在多线程环境中安全共享,无需任何同步操作。
- 可用于哈希键:
String的哈希码(hashCode)在创建时被计算并缓存,因为内容不变,哈希码也不会变,这使得它非常适合用作HashMap的键。 - 字符串常量池 (String Pool):Java可以存储和复用相同的字符串字面量,节省内存。
- 缺点:
- 性能开销:每次修改都会创建新对象,如果在一个循环中进行大量字符串拼接,会产生大量垃圾对象,严重影响性能并给GC(垃圾回收)带来巨大压力。
示例:
String str = "Hello";
str = str + " World"; // 这行代码创建了一个新的String对象,"Hello World",然后让str引用指向它。
// 原来的"Hello"对象仍然存在于内存中,等待被垃圾回收。
2. StringBuffer (线程安全的可变字符串)
StringBuffer 是为了解决 String 不可变性带来的性能问题而设计的。它是一个可变的字符序列。
如何实现可变性?
它的底层也是一个字符数组,但这个数组不是 final 的,并且有预留的容量。当你使用 append()、insert() 等方法修改字符串时,它会直接在原有的字符数组上进行操作。如果数组容量不足,它会自动扩容。
如何保证线程安全?StringBuffer 的几乎所有公开方法(如 append, insert, delete)都使用了 synchronized 关键字进行修饰。
// StringBuffer.java 的部分源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
这意味着在同一时间,只有一个线程能调用 StringBuffer 实例的这些方法。
- 优点:线程安全,可以在多线程环境中安全使用。
- 缺点:由于
synchronized带来的同步锁开销,其性能相较于StringBuilder会差一些。
3. StringBuilder (非线程安全的可变字符串)
StringBuilder 是在 Java 5 中引入的,它在API上与 StringBuffer 完全兼容,但去掉了线程安全保障。
为什么更快?
它的方法没有 synchronized 关键字修饰。在单线程环境下,它省去了加锁和解锁的开销,因此执行效率更高。
示例:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 直接在内部的char[]数组上修改,不会创建新对象
System.out.println(sb.toString()); // 输出 "Hello World"
性能对比演示
来看一个在循环中拼接字符串的经典例子,直观感受性能差异。
public class PerformanceTest {
public static void main(String[] args) {
int iterations = 50000;
// 1. 使用 String
long startTime1 = System.currentTimeMillis();
String str = "";
for (int i = 0; i < iterations; i++) {
str += "item"; // 每次循环都会创建一个新的String对象
}
long endTime1 = System.currentTimeMillis();
System.out.println("String took: " + (endTime1 - startTime1) + " ms");
// 2. 使用 StringBuffer
long startTime2 = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < iterations; i++) {
buffer.append("item");
}
long endTime2 = System.currentTimeMillis();
System.out.println("StringBuffer took: " + (endTime2 - startTime2) + " ms");
// 3. 使用 StringBuilder
long startTime3 = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < iterations; i++) {
builder.append("item");
}
long endTime3 = System.currentTimeMillis();
System.out.println("StringBuilder took: " + (endTime3 - startTime3) + " ms");
}
}
典型输出结果:
String took: 2650 ms
StringBuffer took: 3 ms
StringBuilder took: 1 ms
(具体时间取决于机器性能,但数量级的差异是显而易见的)
结果清晰地表明:StringBuilder > StringBuffer > String
使用场景总结 (如何选择?)
使用
String:- 当字符串内容基本不发生改变时。
- 例如:常量、配置信息、短小的临时变量。
使用
StringBuilder:- 首选! 在单线程环境下,需要对字符串进行频繁的添加、删除等操作时。
- 例如:在循环中拼接字符串、组装JSON/XML、构建复杂的SQL查询语句。
使用
StringBuffer:- 在多线程环境下,当多个线程需要共享并操作同一个字符串对象时。
- 例如:一个被多个线程共享的日志记录器或计数器。
- 注意:这是一种相对少见的需求。在现代并发编程中,通常有更好的方式来处理共享状态(如使用
ConcurrentLinkedQueue然后统一处理,或者使用ThreadLocal),所以StringBuffer的使用频率已经大大降低。
简单来说,遵循以下原则:
- 少量修改 ->
String - 单线程大量修改 ->
StringBuilder - 多线程大量修改 ->
StringBuffer(谨慎使用)