String (不可变字符串)
String 是 Java 中最基本、最常用的字符串类,它的核心特点是 不可变性。

什么是不可变?
一旦一个 String 对象被创建,它的内容(即它所指向的字符序列)就不能被改变,任何对字符串的修改操作(如拼接、替换、截取等)都不会在原对象上进行,而是会 创建一个新的 String 对象 来存储修改后的结果。
为什么 String 要设计成不可变?
- 线程安全不可变,
String对象在多线程环境下是天然安全的,无需额外的同步措施。 - 哈希码缓存:因为
String的内容不会改变,所以它的hashCode()值在对象创建后就可以被缓存,这使得String非常适合用做HashMap、HashSet等哈希集合的键,可以极大地提高性能。 - 字符串常量池:JVM 为了优化性能和内存,会维护一个特殊的内存区域,叫做 字符串常量池,当你使用字面量(如
String s = "hello";)创建String时,JVM 会先检查池中是否已存在该字符串,如果存在,就直接返回引用;如果不存在,就创建新实例并存入池中,这可以避免在内存中创建大量相同的字符串对象。 - 安全性:在许多场景下,字符串被用作网络连接的 URL、文件路径、类名加载器等,不可变性确保了这些关键信息在程序执行过程中不会被恶意或意外地修改。
示例代码:
String s1 = "Hello"; String s2 = s1 + " World"; // 这里不是修改 s1,而是创建了一个新的 String 对象 "Hello World",并让 s2 引用它。 System.out.println(s1); // 输出: Hello System.out.println(s2); // 输出: Hello World // 验证 s1 和 s2 不是同一个对象 System.out.println(s1 == s2); // 输出: false (比较的是内存地址)
适用场景:
- 当字符串的内容不需要改变时,配置信息、常量、作为 Map 的键等。
- 绝大多数字符串处理场景。
StringBuffer (可变、线程安全字符串)
StringBuffer 是一个 可变 的字符序列,你可以修改它所包含的字符序列,而无需创建新的对象。
核心特点:
- 可变性:所有的修改操作(如
append(),insert(),delete()等)都直接在StringBuffer对象本身上进行,不会创建新对象。 - 线程安全:
StringBuffer的所有公共方法都使用了synchronized关键字进行同步,这意味着它在多线程环境下是安全的,可以保证操作的原子性。 - 性能开销:由于线程同步需要额外的开销,
StringBuffer的性能通常比StringBuilder差。
示例代码:
StringBuffer sb = new StringBuffer("Hello");
System.out.println("原始内容: " + sb); // 输出: Hello
// append() 方法直接修改了 sb 对象
sb.append(" World");
System.out.println("追加后: " + sb); // 输出: Hello World
// insert() 方法也是直接修改
sb.insert(5, ", Java");
System.out.println("插入后: " + sb); // 输出: Hello, Java World
// 验证对象没有改变
System.out.println(sb == sb.append("!")); // 输出: true,因为 append 返回的是调用者自身
适用场景:
- 当你需要频繁地修改字符串内容(如在循环中进行拼接)。
- 并且你的代码运行在 多线程 环境下,需要保证线程安全。
StringBuilder (可变、非线程安全字符串)
StringBuilder 是 Java 1.5 引入的,它与 StringBuffer 几乎完全相同,也是一个 可变 的字符序列。
核心特点:
- 可变性:和
StringBuffer一样,所有修改操作都直接在对象本身上进行。 - 非线程安全:
StringBuilder的方法没有synchronized关键字,因此它不是线程安全的。 - 性能更高:由于没有同步开销,
StringBuilder的性能通常优于StringBuffer,特别是在单线程环境下。
示例代码:
(代码与 StringBuffer 示例完全相同,只是类名不同)

StringBuilder sb = new StringBuilder("Hello");
System.out.println("原始内容: " + sb); // 输出: Hello
sb.append(" World");
System.out.println("追加后: " + sb); // 输出: Hello World
sb.insert(5, ", Java");
System.out.println("插入后: " + sb); // 输出: Hello, Java World
System.out.println(sb == sb.append("!")); // 输出: true
适用场景:
- 当你需要频繁地修改字符串内容。
- 并且你的代码运行在 单线程 环境下,或者可以保证在多线程环境下不会有多个线程同时修改同一个
StringBuilder实例。
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全 (因为不可变) | 线程安全 (synchronized) |
非线程安全 |
| 性能 | 低 (频繁修改时,会创建大量新对象) | 较低 (同步开销) | 高 (无同步开销) |
| 主要用途 | 存储和操作不变的字符串 | 多线程环境下频繁修改字符串 | 单线程环境下频繁修改字符串 |
性能对比与最佳实践
让我们通过一个简单的例子来对比三者在频繁拼接时的性能:
public class StringPerformance {
public static void main(String[] args) {
int times = 100000;
// String 性能测试
long startTime = System.currentTimeMillis();
String str = "";
for (int i = 0; i < times; i++) {
str += "a"; // 每次循环都创建一个新的 String 对象
}
long endTime = System.currentTimeMillis();
System.out.println("String 耗时: " + (endTime - startTime) + " ms");
// StringBuffer 性能测试
startTime = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < times; i++) {
sbf.append("a"); // 在原对象上修改
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer 耗时: " + (endTime - startTime) + " ms");
// StringBuilder 性能测试
startTime = System.currentTimeMillis();
StringBuilder sbd = new StringBuilder();
for (int i = 0; i < times; i++) {
sbd.append("a"); // 在原对象上修改
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder 耗时: " + (endTime - startTime) + " ms");
}
}
预期输出结果 (可能因机器不同而有所差异):
String 耗时: 1234 ms
StringBuffer 耗时: 15 ms
StringBuilder 耗时: 5 ms
从结果可以看出,在大量修改操作下,String 的性能非常差,而 StringBuilder 是最快的,StringBuffer 稍慢于 StringBuilder。
最佳实践建议
-
优先使用
StringBuilder:在绝大多数情况下,现代 Java 应用程序都是单线程或通过高级并发工具(如ExecutorService)来管理线程的。StringBuilder应该是你的首选,因为它提供了最佳的性能。
(图片来源网络,侵删) -
仅在必要时使用
StringBuffer:只有在你的代码确实需要在多线程环境下共享和修改同一个字符序列缓冲区时,才使用StringBuffer,这种情况相对较少。 -
避免在循环中使用 拼接
String:如果你确定字符串需要被修改,并且修改操作发生在循环中,绝对不要 使用 运算符,这会导致大量的String对象被创建,从而引发严重的性能问题和内存消耗。 -
对于少量、简单的字符串拼接, 仍然可用:现代 Java 编译器(JIT)对简单的字符串拼接(如
String s = "a" + "b";或少量 操作)有优化,可能会在编译期就合并成一个字符串,但对于循环或动态拼接,StringBuilder仍然是王者。
