RHODES ISLAND // PERSONAL ARCHIVE

关之水瑶 · 作战记录

记录技术、生活与正在进行的思考。欢迎来到我的个人情报终端。

SYSTEM ONLINE / 2026

4. String、StringBuffer、StringBuilder 的区别?为什么 String 设计为不可变?

String、StringBuffer、StringBuilder 的区别,以及 String 为什么设计为不可变

一句话回答

  • String:不可变,每次修改都产生新对象,适合常量、少量拼接、作为 key。
  • StringBuffer:可变,线程安全,方法加了 synchronized,适合多线程下频繁拼接。
  • StringBuilder:可变,线程不安全,但性能最好,适合单线程下频繁拼接。
  • String 不可变:为了安全、共享、缓存、线程安全和性能。

1. 三者核心对比

维度 String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全 安全(不可变天然安全) 安全,方法 synchronized 不安全
性能 拼接差,产生大量临时对象 中等,锁开销 最好
底层存储 private final char[](Java 8)/ byte[](Java 9+) 可扩容 char[] / byte[] 可扩容 char[] / byte[]
是否可继承 final class,不可继承 final class,不可继承 final class,不可继承
适用场景 常量、少量拼接、Map key 多线程频繁拼接 单线程频繁拼接
出现版本 最早 Java 1.0 Java 1.5

记忆:
String 不可变,StringBuffer 安全但慢,StringBuilder 快但不安全。
单线程用 StringBuilder,多线程用 StringBuffer,常量用 String。


2. String 为什么不可变

2.1 不可变的含义

String 对象一旦创建,其内容就不能被修改。
所有看似“修改”的方法,如 substring、concat、replace、toUpperCase,都是返回新对象,原对象不变。

String s = "hello";
String s2 = s.concat(" world");

System.out.println(s);  // hello
System.out.println(s2); // hello world

2.2 如何实现不可变

以 Java 8 为例:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char[] value;

    private int hash; // 缓存 hash
}

关键点:

  1. 类被 final 修饰:不能被继承,防止子类破坏不可变性。
  2. 字符数组 private final:引用不可变,外部无法直接访问。
  3. 不提供修改内部数组的方法:所有“修改”都返回新对象。
  4. 构造时拷贝:避免外部数组引用影响内部状态。
  5. hash 缓存:因为内容不变,hash 只需计算一次。

Java 9 之后改为 byte[] + coder,为了节省内存(Latin-1 字符只用 1 字节),但不可变性不变。


3. String 设计为不可变的原因

3.1 字符串常量池共享,节省内存

字符串是使用最频繁的对象。JVM 用字符串常量池缓存字面量,多个引用可指向同一对象。

String a = "hello";
String b = "hello";
System.out.println(a == b); // true,指向常量池同一对象

如果 String 可变,修改 a 会影响 b,程序行为不可预期。
不可变才能安全共享。

3.2 线程安全

不可变对象天然线程安全,多线程并发读不需要加锁。
因为状态不可变,不存在竞态条件。

3.3 适合作为 HashMap 的 key

HashMap 依赖 hashCode 和 equals。
如果 String 可变,放入 HashMap 后修改内容,hashCode 变化,就再也找不到这个 key,导致内存泄漏和逻辑错误。

Map<String, Integer> map = new HashMap<>();
String key = "a";
map.put(key, 1);

// 如果 String 可变,修改 key 内容后,map.get(key) 可能找不到

不可变保证 key 的 hash 稳定。

3.4 安全性

字符串常用于类名、文件路径、URL、数据库连接、用户名密码等。
如果可变,可能被恶意篡改:

void connect(String url) {
    // 如果 url 在传入后被其他线程修改,可能连接到恶意地址
}

不可变保证传递后内容不会被改变。

3.5 缓存 hash,性能好

String 内部缓存 hash:

private int hash;

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        // 计算并缓存
        hash = h;
    }
    return h;
}

内容不变,hash 只需算一次,作为 HashMap key 时性能好。

3.6 支持协变与安全发布

不可变对象可以安全地在多线程间发布,不需要额外同步。
这也是函数式编程、并发编程中推荐不可变对象的原因。


4. String 拼接的性能问题

4.1 直接用 + 拼接

String s = "";
for (int i = 0; i < 10000; i++) {
    s += i; // 每次循环都创建新 String 和 StringBuilder
}

编译后大致等价于:

for (int i = 0; i < 10000; i++) {
    s = new StringBuilder().append(s).append(i).toString();
}

每次循环都创建新 StringBuilder 和新 String,产生大量临时对象,性能差。

注意:如果是编译期常量拼接,如 "a" + "b",编译器会直接优化成 "ab",不创建中间对象。

4.2 用 StringBuilder

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String s = sb.toString();

只创建一个 StringBuilder,最后转一次 String,性能好得多。

4.3 循环内拼接必须用 StringBuilder

// 差
String result = "";
for (String item : list) {
    result += item;
}

// 好
StringBuilder sb = new StringBuilder();
for (String item : list) {
    sb.append(item);
}
String result = sb.toString();

5. StringBuffer 和 StringBuilder 的实现

两者都继承自 AbstractStringBuilder,核心是一个可扩容的字符数组。

abstract class AbstractStringBuilder {
    char[] value;
    int count;

    public AbstractStringBuilder append(String str) {
        // 扩容、拷贝、追加
        return this;
    }
}

区别在于:

public final class StringBuffer extends AbstractStringBuilder {
    @Override
    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }
}

public final class StringBuilder extends AbstractStringBuilder {
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}
  • StringBuffer 方法加 synchronized,线程安全,但有锁开销。
  • StringBuilder 不加锁,单线程性能更好。

扩容机制

默认容量 16,扩容时通常是 新容量 = 旧容量 * 2 + 2,然后 Arrays.copyOf 拷贝。
如果能预估长度,建议指定初始容量,减少扩容:

StringBuilder sb = new StringBuilder(1024);

6. 如何选择

场景 推荐
字符串常量、少量拼接 String
单线程循环拼接 StringBuilder
多线程共享拼接 StringBuffer
作为 Map key String
需要频繁修改内容 StringBuilder / StringBuffer
需要线程安全且频繁修改 StringBuffer
需要不可变、安全传递 String

7. 面试口述版

String 是不可变的,每次修改都会产生新对象,适合常量和少量拼接。StringBuffer 和 StringBuilder 是可变的,底层是可变字符数组,适合频繁拼接。StringBuffer 方法加了 synchronized,线程安全但有锁开销;StringBuilder 不加锁,单线程性能最好。所以单线程用 StringBuilder,多线程用 StringBuffer。

String 设计为不可变,主要有几个原因:
第一,字符串常量池需要共享,如果可变,一个引用修改会影响其他引用。
第二,不可变天然线程安全,多线程读不需要加锁。
第三,作为 HashMap 的 key,hash 必须稳定,可变会导致 key 找不到。
第四,安全性,字符串常用于 URL、文件路径、类名等,可变可能被恶意篡改。
第五,可以缓存 hash,提升性能。

实现上,String 被 final 修饰,内部字符数组是 private final,不暴露修改方法,所有修改操作都返回新对象。


8. 常见追问

  • String 真的完全不可变吗?
    普通使用下是。通过反射可以修改 private final 数组内容,但这属于破坏封装,不推荐,且 Java 17+ 模块系统限制更强。

  • String s = "a" + "b" 创建几个对象?
    编译期优化为 "ab",只创建 1 个常量池对象(或复用)。

  • new String("abc") 创建几个对象?
    最多 2 个:常量池中的 "abc"(若不存在则创建),以及堆上的新 String 对象。

  • String s = new String("abc") 和 String s = "abc" 区别?
    前者在堆上创建新对象,后者指向常量池;== 比较结果不同。

  • intern() 有什么用?
    把字符串放入常量池并返回池中引用,可用于节省内存。

  • 为什么 Java 9 把 char[] 改成 byte[]?
    节省内存:Latin-1 字符只需 1 字节,char 固定 2 字节。通过 coder 标识编码。

  • StringBuilder 线程不安全会怎样?
    多线程并发 append 可能导致数据丢失、数组越界、内容错乱。

  • 字符串拼接什么时候用 + 也没问题?
    编译期常量拼接、少量拼接、一行内拼接,编译器会优化为 StringBuilder。

posted @ 2026-09-24 17:17  关之水瑶  阅读(17)  评论(0)    收藏  举报