String、StringBuffer、StringBuilder 的区别

摘要

String、StringBuffer、StringBuilder 的区别 分类:知识库 | 标签:Java基础 简介:理清三者底层特性、可变不可变、线程安全、性能差异与业务选型,附带代码示例、字节码佐证和避坑要点。 在 Java 开发中,...

String、StringBuffer、StringBuilder 的区别

分类:知识库 | 标签:Java基础 简介:理清三者底层特性、可变不可变、线程安全、性能差异与业务选型,附带代码示例、字节码佐证和避坑要点。

在 Java 开发中,String、StringBuffer、StringBuilder 都用来表示字符串,但底层设计完全不同,也是面试高频基础考点。核心差异围绕可变性、线程安全、性能三个维度展开。

一、核心特性总览

类是否可变线程安全底层适用场景
String❌ 不可变安全JDK8:final char[];JDK9+:final byte[]字符串内容固定、少量拼接、常量定义
StringBuffer✅ 可变✅ 线程安全可变字符数组,方法加 synchronized多线程环境下字符串拼接
StringBuilder✅ 可变❌ 线程不安全可变字符数组,无同步锁单线程大量拼接(日常开发首选)

可变:对象创建之后,可以直接修改内部字符数组,不会频繁产生新对象。 不可变:底层数组被 final 修饰,一旦对象创建,内容无法修改;所有修改操作都会产生新对象。

二、String:不可变字符串

String 对象一旦创建,内部字符序列不能更改。我们平时看到的 + 拼接,并不是修改原对象,只是语法糖。

示例代码:

public static void main(String[] args) {
    String s = "hello";
    String oldRef = s; // 保存原来对象的引用
    s += " world";

    System.out.println(s == oldRef); // false,指向了新对象
    System.out.println(s); // hello world
}

输出 false,证明拼接之后 s 指向了一个全新的字符串对象,原来的 "hello" 对象没有被改动。

字节码佐证

对 s += " world" 反编译(javap -c)可以看到,底层等价于:

s = new StringBuilder(s).append(" world").toString();

自动新建 StringBuilder 完成拼接,最后调用 toString() 返回新 String。

⚠️ 注意区分常量折叠:

// 编译期直接合并,只会生成一个对象,属于编译器优化
String s = "hello" + " world";

循环内部使用 String += 会反复创建大量对象,产生GC压力,禁止这么写。

三、StringBuffer:可变、线程安全

StringBuffer 内部是可变字符数组,支持 append、insert、delete 等方法直接修改内容; 它的核心方法都加上了 synchronized 同步锁,支持多线程并发操作,但是锁会带来性能损耗。

示例:

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
System.out.println(sb.toString()); // abc

适合场景:多个线程同时往同一个字符串容器追加内容,需要保证并发安全。

开发中这种场景非常少,绝大多数业务不需要它。

四、StringBuilder:可变、非线程安全(推荐)

StringBuilder 和 StringBuffer API 几乎一模一样,底层也是可变数组,区别就是没有 synchronized 锁。 没有锁竞争,所以性能最高,单线程下大量字符串拼接优先选择它。

示例:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(",");
}
String result = sb.toString();
System.out.println(result);

重点提醒:不要在多线程共享同一个 StringBuilder 对象,会出现数据错乱、数组越界等异常。

五、选型总结(直接记这个)

  1. 如果字符串几乎不变、只是常量展示 → 用 String;
  2. 如果是单线程,循环、批量拼接字符串 → 优先 StringBuilder;
  3. 如果是多线程并发,同一个实例被多个线程追加修改 → 使用 StringBuffer;
  4. 循环里禁止 String +=,循环次数多时性能差距非常明显。

六、常见面试追问

Q1:为什么String设计成不可变?

  • 字符串常量池复用,节省内存;
  • 哈希值可以缓存,作为HashMap的key更高效;
  • 线程天然安全;
  • 作为参数传递时,不用担心外部方法修改原字符串。

Q2:StringBuilder和StringBuffer为什么线程安全不一样?

StringBuffer 的 append 等方法加了 synchronized,同一时刻只允许一个线程执行; StringBuilder 无锁,多线程同时append会覆盖内部数组的数据,结果不可预期。

Q3:equals() 和 == 的区别?

  • ==:比较引用地址,判断是不是同一个对象;
  • equals():String重写了该方法,比较字符串内容是否相同。