String & StringBuffer & StringBuilder

对比

特性StringStringBufferStringBuilder
可变性不可变可变可变
线程安全是(天然不可变)是(synchronized方法)
性能低(频繁创建对象)
内存分配每次修改产生新对象动态数组动态数组
初始化容量不可设置默认16,可自定义默认16,可自定义
JDK版本1.01.01.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;
}
  1. 编码处理(coder字段)

    • coder取值0(LATIN1)或1(UTF16BE)

    • 位移操作实现字节与字符转换:

      1
      2
      3
      4
      5
      
      // 字节长度 → 字符容量
      int characters = bytesLength >> coder;
      
      // 字符容量 → 字节长度
      int bytes = characters << coder;
      
  2. 扩容策略

    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字符)
  3. 动态扩容流程

    1
    2
    3
    4
    5
    
    原始数组 → 计算最小需求 →
    ┌─满足需求 → 直接返回
    └─需要扩容 → 计算新容量 →
       ├─超过限制 → 抛出OOM
       └─创建新数组 → 数据复制
    
  4. 性能优化点

    • 延迟计算:只在需要扩容时进行计算
    • 按需扩容:根据实际增长需求动态调整
    • 位运算优化:使用位移代替乘除运算

扩容示例

假设原始状态:

  • 编码方式:UTF16(coder=1)
  • 当前内容:“Hello”(5字符)
  • 当前数组:char[16](初始容量16字符)

执行append(“World!")后:

  1. 需要总字符数:5 + 6 = 11
  2. 当前容量16足够,无需扩容

继续追加数据直到需要17字符:

  1. 计算最小字节需求:17 << 1 = 34字节
  2. 当前数组长度:16 << 1 = 32字节
  3. 计算growth = 34 - 32 = 2字节
  4. 计算prefGrowth:32 + (2 << 1) = 36
  5. 新长度 = 32 + max(2, 36-32) = 32 + 4 = 36字节
  6. 新字符容量: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)
String4236218
StringBuffer120.5
StringBuilder80.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 进行优化。

  1. 常量折叠(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);
        }
    }
    
  2. 字符串拼接使用 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(堆中新对象)

总结

  1. 基础原则

    • 优先考虑不可变性 → String
    • 单线程可变需求 → StringBuilder
    • 多线程可变需求 → StringBuffer
  2. 性能关键点

    • 避免不必要的字符串对象创建
    • 预估容量减少扩容次数
    • 警惕大字符串的内存驻留
  3. 发展趋势

    • Valhalla项目的值类型(inline class)可能带来新的字符串实现
    • GraalVM的字符串优化策略
    • Project Loom对字符串操作的影响

通过合理选择字符串处理类,开发者可以在保证代码质量的同时,显著提升应用程序的性能表现。建议在关键路径代码中结合性能分析工具(如Async Profiler)进行针对性优化。

想都是问题,做才是答案!
使用 Hugo 构建
主题 StackJimmy 设计