String、StringBuilder、StringBuffer 的区别,为什么 String 是不可变的?

在 Java 开发中,字符串操作几乎无处不在。面试时常常会被问到:

  • StringStringBuilderStringBuffer 有什么区别?

  • 为什么 String 是不可变的?

本文带你彻底搞懂这几个类的底层原理和使用场景。


1. 三者的继承关系

先看类图:

java.lang.Object

└── java.lang.String (不可变)

└── java.lang.AbstractStringBuilder

        ├── java.lang.StringBuilder (可变,非线程安全)

        └── java.lang.StringBuffer (可变,线程安全)

结论:

  • String 是不可变类,底层用 final char[](JDK9 之后是 byte[])存储内容。

  • StringBuilderStringBuffer 底层继承自 AbstractStringBuilder,本质是一个 可扩容的字符数组


2. 三者的区别总结

特性StringStringBuilderStringBuffer
可变性❌ 不可变✅ 可变✅ 可变
线程安全✅ 安全(不可变带来的特性)❌ 非线程安全✅ 线程安全(方法加 synchronized
性能低(频繁拼接会产生很多对象)高(适合单线程操作字符串)较低(比 StringBuilder 慢一点)
底层存储结构final char[] / byte[]可扩容 char[] / byte[]可扩容 char[] / byte[]
适用场景不变字符串,如常量、配置、少量拼接高频拼接、单线程环境高频拼接、多线程环境

3. 为什么 String 是不可变的?

3.1 源码分析

String 的核心字段:

public final class String implements java.io.Serializable, Comparable<String> { private final char value[];

private final int hash; // 缓存哈希值 }

  • valuefinal 修饰,指向的数组地址不可变。

  • String 类本身是 final,不能被继承。

  • 修改字符串时,本质上会创建一个新对象。

例如:

String s1 = "hello"; s1 = s1 + " world"; System.out.println(s1);

执行过程:

  1. "hello" 对象被创建并存储在字符串常量池。

  2. " world" 对象被创建。

  3. s1 + " world" 会生成新的字符串 "hello world",并返回给 s1

原来的 "hello" 依然存在,并没有被修改。


3.2 不可变的好处

  1. 安全性

    • String 常用作密码、URL、文件路径等,如果可变,容易被篡改。

  2. 线程安全

    • 多个线程共享同一个字符串对象时,不需要额外同步。

  3. 提高哈希效率

    • 不可变性保证 hashCode 一旦计算就不会变化,所以 String 可以安全地作为 HashMapHashSet 的 key。

  4. 字符串常量池优化

    • JVM 可以将相同的字符串对象复用,节省内存。


4. 使用场景对比

✅ String

适合少量字符串拼接、固定值:

String name = "Alice"; String msg = "Hello " + name;

✅ StringBuilder

适合单线程环境下的大量拼接:

StringBuilder sb = new StringBuilder();

for (int i = 0; i < 10000; i++) { sb.append(i); } System.out.println(sb.toString());

✅ StringBuffer

适合多线程环境下的大量拼接:

StringBuffer sb = new StringBuffer();

for (int i = 0; i < 10000; i++) { sb.append(i); } System.out.println(sb.toString());


5. 性能对比测试

public class StringTest {
    public static void main(String[] args) {
        int count = 100000;

        // String
        long start = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < count; i++) {
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("String耗时: " + (end - start));

        // StringBuilder
        start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuilder耗时: " + (end - start));

        // StringBuffer
        start = System.currentTimeMillis();
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < count; i++) {
            sbf.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuffer耗时: " + (end - start));
    }
}

典型输出结果(机器不同会有差异):

String耗时: 3000ms+
StringBuilder耗时: 20ms
StringBuffer耗时: 30ms

结论:

  • String 性能最差,因为频繁创建新对象。

  • StringBuilder > StringBuffer,单线程情况下推荐使用 StringBuilder


6. 总结

  • String:不可变,线程安全,适合少量拼接、常量场景。

  • StringBuilder:可变,非线程安全,性能最好,推荐在单线程拼接时使用。

  • StringBuffer:可变,线程安全,适合多线程拼接场景。

面试时答题思路:
先讲三者区别,再说为什么 String 不可变(final 修饰、类是 final、数组引用不可变),最后补充不可变的好处(安全、哈希、常量池)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值