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对象,会出现数据错乱、数组越界等异常。
五、选型总结(直接记这个)
- 如果字符串几乎不变、只是常量展示 → 用
String; - 如果是单线程,循环、批量拼接字符串 → 优先
StringBuilder; - 如果是多线程并发,同一个实例被多个线程追加修改 → 使用
StringBuffer; - 循环里禁止
String +=,循环次数多时性能差距非常明显。
六、常见面试追问
Q1:为什么String设计成不可变?
- 字符串常量池复用,节省内存;
- 哈希值可以缓存,作为HashMap的key更高效;
- 线程天然安全;
- 作为参数传递时,不用担心外部方法修改原字符串。
Q2:StringBuilder和StringBuffer为什么线程安全不一样?
StringBuffer 的 append 等方法加了 synchronized,同一时刻只允许一个线程执行;
StringBuilder 无锁,多线程同时append会覆盖内部数组的数据,结果不可预期。
Q3:equals() 和 == 的区别?
==:比较引用地址,判断是不是同一个对象;equals():String重写了该方法,比较字符串内容是否相同。