在Java开发中,String、StringBuffer和StringBuilder是处理文本的核心类。选择不当会引发性能问题和线程安全隐患。下面通过对比介绍和代码示例,帮助你精准选用。
下面的表格直观对比了这三个类的核心差异,方便您快速把握重点。
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
| 线程安全 | ✅ 安全(因不可变) | ❌ 不安全 | ✅ 安全(synchronized实现) |
| 性能 | 低(频繁修改时) | 高(单线程最佳) | 中(有同步开销) |
| 适用场景 | 常量、少量操作 | 单线程下大量修改 | 多线程下大量修改 |
简单来说:
String 对象一旦创建,其内容就无法更改。任何看似修改的操作(如拼接、替换)都会创建一个新的String对象,原有的对象不会改变。String 对象是天然线程安全的,可以在多线程间安全共享。HashMap 等集合的键(因其不可变性保证了哈希值的稳定)。StringBuilder 是可变的字符序列。它可以在原对象上进行修改,而不会创建新的对象。synchronized 关键字修饰,因此在多线程环境下使用可能导致数据不一致。StringBuffer 也是可变的字符序列,基本功能与 StringBuilder 相同。synchronized 关键字进行同步,从而保证在多线程环境下的操作是安全的。StringBuilder。这是最常见且对性能影响最大的错误之一。
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次循环都会创建新的String对象!
}
问题:在循环体内,每次 += 操作都会创建一个新的 String 对象(包含之前的全部内容和新增内容)。循环1万次将产生1万个临时对象,极度浪费内存和CPU。
使用 StringBuilder 来替代。
StringBuilder sb = new StringBuilder(); // 可预估最终长度时,建议指定初始容量,如new StringBuilder(20000)
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
优势:自始至终只操作一个 StringBuilder 对象,通过其内部的字符数组进行扩展,性能极高。
// 在一个明确的单线程方法中
public String createGreeting(String name) {
StringBuffer sb = new StringBuffer(); // 没有必要使用线程安全的StringBuffer
sb.append("Hello, ");
sb.append(name);
sb.append("!");
return sb.toString();
}
问题:在局部方法变量(不存在线程共享)中使用了 StringBuffer,其同步锁 synchronized 成为了不必要的性能开销。
改用更轻量的 StringBuilder。
public String createGreeting(String name) {
StringBuilder sb = new StringBuilder(); // 单线程下性能更优
sb.append("Hello, ");
sb.append(name);
sb.append("!");
return sb.toString();
}
StringBuilder sb = new StringBuilder(); // 使用默认容量(16)
for (int i = 0; i < 1000; i++) {
sb.append("a very long string..."); // 可能导致多次扩容
}
问题:StringBuilder 和 StringBuffer 内部有容量概念。默认容量较小(如16字符),当追加的内容超过当前容量时,会触发扩容(创建新数组,复制旧数据)。频繁扩容影响性能。
如果能预估大致的最终大小,应在创建时指定初始容量。
// 预估最终字符串长度大约为 1000 * 20 = 20000 字符
StringBuilder sb = new StringBuilder(20000);
for (int i = 0; i < 1000; i++) {
sb.append("a very long string...");
}
public class ThreadUnsafeExample {
private static StringBuilder sharedBuilder = new StringBuilder(); // 非线程安全的对象被多个线程共享
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { sharedBuilder.append("Thread1 "); });
Thread t2 = new Thread(() -> -> { sharedBuilder.append("Thread2 "); });
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sharedBuilder.toString()); // 输出结果可能不一致或不完整
}
}
问题:多个线程同时操作非线程安全的 StringBuilder,可能导致数据丢失、脏读或异常。
private static StringBuffer sharedBuffer = new StringBuffer(); // 线程安全
StringBuilder):
private static final ThreadLocal<StringBuilder> localBuilder = ThreadLocal.withInitial(StringBuilder::new);
// 在每个线程内部调用 localBuilder.get().append(...)
选型口诀:
简单拼接优化:对于一次性拼接少量字符串(如 "Hello, " + name),现代Java编译器(JDK5+)会自动优化为 StringBuilder 操作,这种情况下直接使用 + 可读性更好,无需刻意创建 StringBuilder。
API高效使用:熟练使用 append(), insert(), delete(), reverse() 等方法,可以更灵活地操作字符串。
理解 String、StringBuilder 和 StringBuffer 的本质区别是写出高效、健壮Java程序的关键。请记住这个核心原则:根据字符串的“变化频率”和代码的“线程环境”来做出选择。在大多数现代应用中,由于多数代码运行在单线程上下文(如Web请求的处理),StringBuilder 成为了最常用的选择。
希望这份详细的指南能帮助你在实际开发中做出正确的选择。