对比
特性 | String | StringBuffer | StringBuilder |
---|
可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是(天然不可变) | 是(synchronized方法) | 否 |
性能 | 低(频繁创建对象) | 中 | 高 |
内存分配 | 每次修改产生新对象 | 动态数组 | 动态数组 |
初始化容量 | 不可设置 | 默认16,可自定义 | 默认16,可自定义 |
JDK版本 | 1.0 | 1.0 | 1.5 |
使用场景 | 常量字符串、配置信息 | 多线程环境字符串操作 | 单线程环境字符串操作 |
实现分析
Java字符串体系结构
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌───────────┐ ┌───────────────────────┐
│ String │ │ AbstractStringBuilder │
│-----------│ │-----------------------│
│ - value[] │<──────>│ + value[] │
│ - hash │ │ + count │
└────┬──────┘ └──────┬────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ StringBuffer │ │ StringBuilder │
│---------------│ │-----------------│
│ + sync methods│ │ - non-sync │
└───────────────┘ └─────────────────┘
|
String
1
2
3
4
5
6
7
8
9
10
11
| public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 实际存储数据的不可变数组 */
// jdk9 之前
private final char value[];
// jdk9 之后
private final byte[] value;
/** 字符串的哈希码缓存 */
private int hash; // Default to 0
}
|
- 不可变性:value数组被声明为final,任何修改都会创建新对象
- 内存优化:从JDK9开始改用byte[]存储,支持LATIN1/UTF-16编码
- 常量池:字符串字面量自动加入常量池,减少重复创建
StringBuffer & StringBuilder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| abstract class AbstractStringBuilder {
/** 动态数组存储字符数据 */
byte[] value;
/** 当前已使用的字符数 */
int count;
}
public final class StringBuffer
extends AbstractStringBuilder
implements Serializable, CharSequence {
// 所有方法添加synchronized关键字
@Override
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
}
public final class StringBuilder
extends AbstractStringBuilder
implements Serializable, CharSequence {
// 非线程安全实现
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
|
- 动态扩容:初始容量16(字符数)
- 线程安全:StringBuffer通过方法级同步保证线程安全(
synchronized
关键字修饰) - 继承抽象类:StringBuffer和StringBuilder都继承了AbstractStringBuilder
扩容分析,基于jdk17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| // 公开的容量确认方法
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0) { // 过滤无效参数
ensureCapacityInternal(minimumCapacity); // 调用内部扩容逻辑
}
}
// 内部扩容实现
private void ensureCapacityInternal(int minimumCapacity) {
// 计算当前字符容量(字节长度 >> 编码位数)
int oldCapacity = value.length >> coder;
// 需要扩容的条件判断
if (minimumCapacity - oldCapacity > 0) {
// 创建新数组并复制数据
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
// 新容量计算逻辑
private int newCapacity(int minCapacity) {
// 当前数组的字节长度
int oldLength = value.length;
// 计算所需的最小字节长度(字符数 << 编码位数)
int newLength = minCapacity << coder;
// 需要扩展的字节数
int growth = newLength - oldLength;
// 动态计算新容量(核心扩容算法)
int length = ArraysSupport.newLength(oldLength,
growth,
oldLength + (2 << coder)); // 默认扩展量
// 处理最大容量限制
if (length == Integer.MAX_VALUE) {
throw new OutOfMemoryError(...);
}
// 返回字符容量(字节长度 >> 编码位数)
return length >> coder;
}
|
编码处理(coder字段):
扩容策略:
1
2
3
4
5
6
7
| ArraysSupport.newLength(
int oldLength, // 当前数组长度(字节)
int minGrowth, // 至少需要增长的量
int prefGrowth // 推荐增长量
)
int length = ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));
|
- 实际扩容公式:
新长度 = oldLength + max(minGrowth, prefGrowth)
- 默认扩展量计算:
prefGrowth = oldLength + (2 << coder)
- LATIN1编码时:+2字节(即扩容2字符)
- UTF16编码时:+4字节(即扩容2字符)
动态扩容流程:
1
2
3
4
5
| 原始数组 → 计算最小需求 →
┌─满足需求 → 直接返回
└─需要扩容 → 计算新容量 →
├─超过限制 → 抛出OOM
└─创建新数组 → 数据复制
|
性能优化点:
- 延迟计算:只在需要扩容时进行计算
- 按需扩容:根据实际增长需求动态调整
- 位运算优化:使用位移代替乘除运算
扩容示例
假设原始状态:
- 编码方式:UTF16(coder=1)
- 当前内容:“Hello”(5字符)
- 当前数组:char[16](初始容量16字符)
执行append(“World!")后:
- 需要总字符数:5 + 6 = 11
- 当前容量16足够,无需扩容
继续追加数据直到需要17字符:
- 计算最小字节需求:17 << 1 = 34字节
- 当前数组长度:16 << 1 = 32字节
- 计算growth = 34 - 32 = 2字节
- 计算prefGrowth:32 + (2 << 1) = 36
- 新长度 = 32 + max(2, 36-32) = 32 + 4 = 36字节
- 新字符容量:36 >> 1 = 18字符
最终完成从16到18字符的扩容,实际扩容量是原始容量的1.125倍,而非传统的双倍扩容。
性能测试
测试场景
10万次字符串追加操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| public class PerformanceTest {
static final int LOOP_COUNT = 100_000;
public static void main(String[] args) {
// String测试
long start1 = System.nanoTime();
String s = "";
for (int i = 0; i < LOOP_COUNT; i++) {
s += "a";
}
long duration1 = (System.nanoTime() - start1) / 1_000_000;
// StringBuffer测试
long start2 = System.nanoTime();
StringBuffer sbuf = new StringBuffer();
for (int i = 0; i < LOOP_COUNT; i++) {
sbuf.append("a");
}
long duration2 = (System.nanoTime() - start2) / 1_000_000;
// StringBuilder测试
long start3 = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < LOOP_COUNT; i++) {
sb.append("a");
}
long duration3 = (System.nanoTime() - start3) / 1_000_000;
System.out.printf("String: %dms%n", duration1);
System.out.printf("StringBuffer: %dms%n", duration2);
System.out.printf("StringBuilder: %dms%n", duration3);
}
}
|
测试结果
JDK17,Mac M1
实现方式 | 耗时(ms) | 内存分配(MB) |
---|
String | 4236 | 218 |
StringBuffer | 12 | 0.5 |
StringBuilder | 8 | 0.3 |
最佳实践
选择策略
优先使用String:
- 存储常量配置信息
- 作为方法参数传递
- 需要作为Map的Key使用时
使用StringBuilder:
- 单线程环境下字符串拼接
- SQL语句动态构建
- 日志消息组装
使用StringBuffer:
- 多线程共享的字符串操作
- 全局日志缓冲区
- 需要同步修改的共享资源
性能优化技巧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 预分配容量(减少扩容次数)
StringBuilder sb = new StringBuilder(1024);
// 链式调用优化
String result = new StringBuilder()
.append("Name: ").append(user.getName())
.append(", Age: ").append(user.getAge())
.toString();
// 避免在循环中使用字符串拼接
// 错误示例:
String output = "";
for (Data data : list) {
output += data.getValue(); // 产生大量临时对象
}
// 正确示例:
StringBuilder output = new StringBuilder();
for (Data data : list) {
output.append(data.getValue());
}
|
特殊场景处理
多线程安全操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 使用StringBuffer的同步控制
class SharedResource {
private StringBuffer buffer = new StringBuffer();
public void safeAppend(String str) {
synchronized(buffer) {
buffer.append(str);
}
}
}
// 或使用ThreadLocal
private ThreadLocal<StringBuilder> localBuilder =
ThreadLocal.withInitial(() -> new StringBuilder(256));
|
常见误区
误区1:StringBuilder一定比StringBuffer快
- 真相:在单线程环境下确实如此,但差异通常在微秒级。实际开发中更应关注代码可读性
误区2:StringBuffer可以完全替代StringBuilder
- 线程开销:StringBuffer的同步锁在竞争激烈时会导致性能骤降
- 对象复用:StringBuffer实例作为类成员时可能被错误共享
进阶
JDK8优化改进
在 Java 8 之后,编译器对 String
常量拼接进行了优化,主要是通过 字符串常量池 和 StringBuilder
进行优化。
常量折叠(Constant Folding):
- 编译器会在编译时将
String
的常量拼接计算出结果。这意味着在代码编译时,所有的常量字符串拼接都会被直接合并为一个常量字符串。 - 这种优化可以减少运行时的计算,因为常量拼接的结果已经在编译时得到了处理。
1
2
3
4
5
6
| public class StringConcatenation {
public static void main(String[] args) {
String result = "Hello" + " " + "World"; // 这是常量拼接
System.out.println(result);
}
}
|
编译器会直接把 "Hello" + " " + "World"
计算为 "Hello World"
,因此代码最终会变成:
1
2
3
4
5
6
| public class StringConcatenation {
public static void main(String[] args) {
String result = "Hello World";
System.out.println(result);
}
}
|
字符串拼接使用 StringBuilder
:
- 对于运行时的拼接,编译器会自动使用
StringBuilder
来优化多个字符串的拼接操作,避免了重复创建临时 String
对象。
1
2
3
4
5
6
7
8
| public class StringConcatenation {
public static void main(String[] args) {
String a = "Hello";
String b = "World";
String result = a + " " + b; // 运行时拼接
System.out.println(result);
}
}
|
编译器会优化为使用 StringBuilder
,最终生成的字节码类似于:
1
2
3
4
5
6
7
8
| public class StringConcatenation {
public static void main(String[] args) {
String a = "Hello";
String b = "World";
String result = new StringBuilder().append(a).append(" ").append(b).toString();
System.out.println(result);
}
}
|
JDK9优化改进
- 紧凑字符串:根据内容自动选择Latin-1或UTF-16编码
- 字符串去重:G1垃圾收集器的字符串去重功能(-XX:+UseStringDeduplication)
内存泄漏防范
1
2
3
4
5
6
7
8
9
10
| // 大字符串处理示例
void processHugeData() {
String hugeString = readHugeFile(); // 1MB字符串
// 错误用法:截取小部分但保留大数组
String subStr = hugeString.substring(0,10);
// 正确做法:显式创建新字符串
subStr = new String(hugeString.substring(0,10));
}
|
字符串池机制
1
2
3
4
5
6
| String s1 = "java";
String s2 = "java";
String s3 = new String("java");
System.out.println(s1 == s2); // true(常量池引用)
System.out.println(s1 == s3); // false(堆中新对象)
|
总结
基础原则:
- 优先考虑不可变性 → String
- 单线程可变需求 → StringBuilder
- 多线程可变需求 → StringBuffer
性能关键点:
- 避免不必要的字符串对象创建
- 预估容量减少扩容次数
- 警惕大字符串的内存驻留
发展趋势:
- Valhalla项目的值类型(inline class)可能带来新的字符串实现
- GraalVM的字符串优化策略
- Project Loom对字符串操作的影响
通过合理选择字符串处理类,开发者可以在保证代码质量的同时,显著提升应用程序的性能表现。建议在关键路径代码中结合性能分析工具(如Async Profiler)进行针对性优化。