Java 字符串详解:String、StringBuilder 与 StringBuffer
字符串是 Java 中最常用的数据类型之一,Java 提供了 String、StringBuilder 和 StringBuffer 三个类用于处理字符串,但它们的特性和适用场景有显著差异。本文将深入解析这三个类的底层实现、核心特性及最佳实践。
String 类:不可变的字符串
String 类是 Java 中最基础的字符串类,其核心特性是不可变性,这一特性深刻影响了它的使用方式和性能表现。
不可变性的底层实现
String 类被 final 修饰,且其底层存储字符的数组 value 也被 final 修饰,因此字符串对象一旦创建,其内容不可修改。
1 | public final class String implements java.io.Serializable, Comparable<String>, CharSequence { |
不可变性的含义:字符串的任何修改操作(如拼接、替换)都会创建新的String对象,原对象内容保持不变。
1
2String s = "hello";
s += " world"; // 生成新对象"hello world",原对象"hello"仍存在
字符串常量池:优化内存占用
为减少重复字符串的内存消耗,Java 引入了字符串常量池(位于方法区),用于存储编译期确定的字符串常量。
两种创建字符串的方式对比:
| 创建方式 | 原理 | 示例 |
|---|---|---|
| 直接赋值(字面量) | 优先从常量池查找,若存在则返回引用;否则创建常量并放入池,再返回引用。 | String s = "abc"; |
new String() 构造器 |
始终在堆中创建新对象,若常量池无对应常量则同时创建并放入池。 | String s = new String("abc"); |
字节码分析:new String("xyz") 的对象创建
1 | // 首次创建"xyz" |
字节码显示:
ldc #3:从常量池加载 “xyz”(若不存在则创建)。new #2:在堆中创建String对象。- 最终结果:1 个常量池对象 + 1 个堆对象(共 2 个对象)。
若常量池已存在 “xyz”(如之前有 String ss = "xyz";),则仅创建1 个堆对象。
字符串拼接的底层机制
字符串拼接(+ 运算符)的底层实现因场景而异:
- 编译期确定的拼接(如
"a" + "b"):直接优化为常量"ab",不产生额外对象。 - 运行期动态拼接(如变量拼接):Java 8 及以上会编译为
StringBuilder的append操作,最后调用toString()生成新String对象。
示例:循环拼接的性能问题
1 | String s = null; |
字节码显示,每次循环都会:
- 创建
StringBuilder对象。 - 调用
append拼接字符串和数字。 - 调用
toString()生成新String对象。
性能损耗:循环次数越多,创建的临时对象越多,内存和时间开销越大。
intern() 方法:手动入池
intern() 方法用于将字符串对象加入常量池(或返回池中已有对象的引用),其行为在 JDK 6 和 JDK 7+ 中有差异:
- JDK 6:若常量池无此字符串,将字符串对象复制到常量池并返回池中引用;否则直接返回池中引用。
- JDK 7+:若常量池无此字符串,将堆中对象的引用存入常量池并返回该引用;否则直接返回池中引用。
示例:intern() 的使用
1 | String s1 = "aa"; |
String.format():格式化字符串
String.format() 用于格式化字符串,内部通过 Formatter 类实现,支持多种格式占位符(如 %s、%d、%.2f)。
1 | double price = 9.9; |
StringBuilder 与 StringBuffer:可变字符串
StringBuilder 和 StringBuffer 是可变字符串类,底层通过可扩容的字符数组存储数据,适合频繁修改字符串的场景(如拼接、插入、删除)。
核心特性对比
| 特性 | StringBuilder(JDK 5+) |
StringBuffer(JDK 1.0+) |
|---|---|---|
| 线程安全性 | 不安全(无同步机制) | 安全(方法被 synchronized 修饰) |
| 性能 | 高(无同步开销) | 较低(同步开销) |
| 适用场景 | 单线程环境 | 多线程环境 |
| 底层实现 | 可变字符数组(char[] value) |
可变字符数组(char[] value) |
初始化与扩容机制
- 初始容量:默认 16 个字符,可通过构造器指定初始容量(如
new StringBuilder(100))。 - 扩容规则:当字符数量超过当前容量时,新容量 =
oldCapacity * 2 + 2,并将原数组内容复制到新数组。
性能优化:若已知字符串大致长度,建议初始化时指定容量,避免频繁扩容(复制数组耗时)。
常用方法
| 方法 | 功能描述 | 示例 |
|---|---|---|
append(...) |
追加数据(字符、字符串等) | sb.append("hello").append(123); |
insert(int, ...) |
在指定位置插入数据 | sb.insert(5, " world"); |
delete(int, int) |
删除指定范围的字符 | sb.delete(5, 11); |
replace(int, int, String) |
替换指定范围的字符 | sb.replace(0, 5, "hi"); |
reverse() |
反转字符串 | sb.reverse(); |
toString() |
转换为 String 对象 |
String s = sb.toString(); |
与 String 的性能对比
频繁修改字符串时,StringBuilder 性能远优于 String:
1 | // 测试10万次拼接的耗时 |
三者的核心区别与选择
| 类 | 不可变性 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|---|
String |
是 | 安全(不可变) | 低(修改时创建新对象) | 字符串不频繁修改、作为常量或参数传递 |
StringBuilder |
否 | 不安全 | 高 | 单线程环境、频繁修改字符串(如拼接、插入) |
StringBuffer |
否 | 安全 | 中 | 多线程环境、频繁修改字符串(如并发场景) |
最佳实践
- 优先使用
String作为参数:不可变性保证了传递过程中内容不被修改,适合作为方法参数或常量。 - 单线程修改用
StringBuilder:避免String的性能损耗和StringBuffer的同步开销。 - 多线程修改用
StringBuffer:通过同步机制保证线程安全(或使用java.util.concurrent包中的并发工具)。 - 指定初始容量:使用
StringBuilder或StringBuffer时,若已知字符串长度,初始化时指定容量(如new StringBuilder(1024)),减少扩容次数。 - 避免字符串拼接在循环中:循环内使用
StringBuilder替代String的+运算符,大幅提升性能