0%

字符串

Java 字符串详解:String、StringBuilder 与 StringBuffer

字符串是 Java 中最常用的数据类型之一,Java 提供了 StringStringBuilderStringBuffer 三个类用于处理字符串,但它们的特性和适用场景有显著差异。本文将深入解析这三个类的底层实现、核心特性及最佳实践。

String 类:不可变的字符串

String 类是 Java 中最基础的字符串类,其核心特性是不可变性,这一特性深刻影响了它的使用方式和性能表现。

不可变性的底层实现

String 类被 final 修饰,且其底层存储字符的数组 value 也被 final 修饰,因此字符串对象一旦创建,其内容不可修改。

1
2
3
4
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[]; // 存储字符串的字符数组(final修饰)
// ... 其他代码
}
  • 不可变性的含义:字符串的任何修改操作(如拼接、替换)都会创建新的String对象,原对象内容保持不变。

    1
    2
    String s = "hello";
    s += " world"; // 生成新对象"hello world",原对象"hello"仍存在

字符串常量池:优化内存占用

为减少重复字符串的内存消耗,Java 引入了字符串常量池(位于方法区),用于存储编译期确定的字符串常量。

两种创建字符串的方式对比:
创建方式 原理 示例
直接赋值(字面量) 优先从常量池查找,若存在则返回引用;否则创建常量并放入池,再返回引用。 String s = "abc";
new String() 构造器 始终在堆中创建新对象,若常量池无对应常量则同时创建并放入池。 String s = new String("abc");
字节码分析:new String("xyz") 的对象创建
1
2
// 首次创建"xyz"
String s = new String("xyz");

字节码显示:

  • ldc #3:从常量池加载 “xyz”(若不存在则创建)。
  • new #2:在堆中创建 String 对象。
  • 最终结果:1 个常量池对象 + 1 个堆对象(共 2 个对象)。

若常量池已存在 “xyz”(如之前有 String ss = "xyz";),则仅创建1 个堆对象

字符串拼接的底层机制

字符串拼接(+ 运算符)的底层实现因场景而异:

  • 编译期确定的拼接(如 "a" + "b"):直接优化为常量 "ab",不产生额外对象。
  • 运行期动态拼接(如变量拼接):Java 8 及以上会编译为 StringBuilderappend 操作,最后调用 toString() 生成新 String 对象。
示例:循环拼接的性能问题
1
2
3
4
String s = null;
for (int i = 0; i < 10; i++) {
s = s + i; // 每次循环都会创建新的StringBuilder和String对象
}

字节码显示,每次循环都会:

  1. 创建 StringBuilder 对象。
  2. 调用 append 拼接字符串和数字。
  3. 调用 toString() 生成新 String 对象。

性能损耗:循环次数越多,创建的临时对象越多,内存和时间开销越大。

intern() 方法:手动入池

intern() 方法用于将字符串对象加入常量池(或返回池中已有对象的引用),其行为在 JDK 6 和 JDK 7+ 中有差异:

  • JDK 6:若常量池无此字符串,将字符串对象复制到常量池并返回池中引用;否则直接返回池中引用。
  • JDK 7+:若常量池无此字符串,将堆中对象的引用存入常量池并返回该引用;否则直接返回池中引用。
示例:intern() 的使用
1
2
3
4
5
6
7
8
9
10
String s1 = "aa";
String s2 = "bb";
String s3 = "aabb"; // 常量池对象
String s4 = s1 + s2; // 堆对象(运行期拼接)
String s5 = "aa" + "bb"; // 编译期优化为"aabb"(常量池对象)
String s6 = s4.intern(); // 返回常量池中的"aabb"引用

System.out.println(s3 == s4); // false(s3在池,s4在堆)
System.out.println(s3 == s5); // true(均为池对象)
System.out.println(s3 == s6); // true(s6指向池对象)

String.format():格式化字符串

String.format() 用于格式化字符串,内部通过 Formatter 类实现,支持多种格式占位符(如 %s%d%.2f)。

1
2
3
double price = 9.9;
String formatted = String.format("商品价格:%.2f元", price);
System.out.println(formatted); // 输出:商品价格:9.90元

StringBuilder 与 StringBuffer:可变字符串

StringBuilderStringBuffer可变字符串类,底层通过可扩容的字符数组存储数据,适合频繁修改字符串的场景(如拼接、插入、删除)。

核心特性对比

特性 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 测试10万次拼接的耗时
long start = System.currentTimeMillis();

// String方式(性能差)
String s = "";
for (int i = 0; i < 100000; i++) {
s += i;
}

// StringBuilder方式(性能好)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(i);
}

long end = System.currentTimeMillis();
System.out.println("String耗时:" + (end - start) + "ms"); // 约几秒
System.out.println("StringBuilder耗时:" + (end - start) + "ms"); // 约几毫秒

三者的核心区别与选择

不可变性 线程安全 性能 适用场景
String 安全(不可变) 低(修改时创建新对象) 字符串不频繁修改、作为常量或参数传递
StringBuilder 不安全 单线程环境、频繁修改字符串(如拼接、插入)
StringBuffer 安全 多线程环境、频繁修改字符串(如并发场景)

最佳实践

  1. 优先使用 String 作为参数:不可变性保证了传递过程中内容不被修改,适合作为方法参数或常量。
  2. 单线程修改用 StringBuilder:避免 String 的性能损耗和 StringBuffer 的同步开销。
  3. 多线程修改用 StringBuffer:通过同步机制保证线程安全(或使用 java.util.concurrent 包中的并发工具)。
  4. 指定初始容量:使用 StringBuilderStringBuffer 时,若已知字符串长度,初始化时指定容量(如 new StringBuilder(1024)),减少扩容次数。
  5. 避免字符串拼接在循环中:循环内使用 StringBuilder 替代 String+ 运算符,大幅提升性能

欢迎关注我的其它发布渠道