jdk 源码系列之StringBuilder、StringBuffer

Posted by Sinsy on September 29, 2020 About 5 k words and Need 15 min

jdk 源码系列之StringBuilder、StringBuffer

前言

StringBuilderStringBuffer 经常使用到,分析 StringBuilderStringBuffer 源码、通过对比加深对这两个类的了解,以及以后更好的使用。

父类

1
2
3
4
5
6
7
8
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence


 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

从上面的继承,我们可以知道,无论是 StringBuilder 亦或是 StringBuffer 都是继承相同的父类 AbstractStringBuilder ,这说明 StringBuilderStringBuffer 他们都是 AbstractStringBuilder 子类,而且操作继承父类的功能相同,差异在于子类实现的不同。我们先来看看 AbstractStringBuilder 的具体实现。

AbstractStringBuilder

先来看看构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * This no-arg constructor is necessary for serialization of subclasses.
 */
AbstractStringBuilder() {
}

/**
 * This no-arg constructor is necessary for serialization of subclasses.
 */
AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

提供两个构造函数,一个是无参构造、一个是有参构造。其中,有参构造,初始化了 char[] 的大小。

了解到这里够了,我们来看看 StringBuilder 以及 StringBuffer 这些子类,具体调了 AbstractStringBuilder 哪些方法。

StringBuilder

提供三种 new 对象

1
2
3
4
5
StringBuilder stringBuilder = new StringBuilder();

StringBuilder stringBuilder = new StringBuilder("sin sy");

StringBuilder stringBuilder = new StringBuilder(10);

点进去

1
2
3
4
5
6
7
8
9
10
11
12
13
public StringBuilder() {
    super(16);
}

public StringBuilder(int capacity) {
    super(capacity);
}

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

前面的三种构造、一次调用如上图所示。 调用用了父类的 AbstractStringBuilder 构造构造方法,初始化 char[] 的大小。看来是默认 char[] 是 16。也就是说 StringBuilder 默认大小是16。以及提供初始化 char[] 大小。如果传入的 String 类型,则 char[] 的大小是字符串的长度 + 16。

这里和 HashMap 差不多,都有初始化大小。可能有几个问题,比如使用 StringBuilder 的时候,拼接字符串的长度很小,远远没到 16 的长度。导致空置了许多 char 空间,而这些没用被引用的空间,会触发 GC 回收,进而可能触发 Full GC 影响整个程序性能,所以如果能知道具体长度,尽可能的指定初始化值,优化性能。

接下来点开看 append 源码。

1
2
3
4
5
6
7
8
9
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

进来的时候,首先判断是不是 null,如果是的话,直接写上 null,标记当前 char 的位置。

1
2
3
4
5
6
7
8
9
10
11
private AbstractStringBuilder appendNull() {
    int c = count;
    ensureCapacityInternal(c + 4);
    final char[] value = this.value;
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;
    return this;
}

使用了类的成员变量 count

1
2
3
4
5
6
7
8
9
/**
 * The value is used for character storage.
 */
char[] value;

/**
 * The count is the number of characters used.
 */
int count;

其中 value 就是字符存储空间,初始化的使用用到。

count 是记录字符数组的位置。我看了一会,没有找到初始化这个的值,那么这里的 count = 0。

继续, 用到了 ensureCapacityInternal ,看看里面有啥,不过看英文名叫做 确保内部容量。应该是确认容量大小。

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
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    // 判断扩容
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    // 扩容处理,扩当前容器的 2倍 + 2 容
    int newCapacity = (value.length << 1) + 2;

    // 判断当前的字符串大小,是否超过扩容之后的大小
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }

    // 超过最大数量,则直接直接抛出 OOM,反之则直接使用扩容后的大小
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}


private int hugeCapacity(int minCapacity) {
    if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
        throw new OutOfMemoryError();
    }
    return (minCapacity > MAX_ARRAY_SIZE)
        ? minCapacity : MAX_ARRAY_SIZE;
}

当处理的字符串长度,大于当前所能容下的大小,则以 当前容量 * 2 + 2 大小扩容,反之则不能处理。同时进行数组间的复制。这也是什么 StringBuilder 能动态拼接字符串,而不是固定大小就不能再申请的原因。

当超过所能容纳的最大字符时,2 ** 31 - 9 (MAX_VALUE = 2 **31 -1,另外它自己 - 8) 的大小,则直接 OOM。

由于这里存在动态扩容,char[] 的空间也可能存在空置,未被使用,引起 GC 回收。同时这里还有数组间的复制,导致性能有所下降,所以还是能确定大小,则直接初始化大小。

继续 append 方法。

appendNull 和 append 差不过,每次都在判断是否需要扩容,然后记录 char[] 的位置。

最后,一般我们都要将 StringBuilder toString()。

1
2
3
4
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

输出的时候,直接使用 char[] 数组,从 0 到 count 所记录的位置,产生一个对象 String 返回。

另外。

StringBuilder 还有提供了许多有趣的东西。

  • 字符串的反转
  • StringBuilder 类拼接
  • 插入某个位置
  • 以及返回当前数组位置 等等

StringBuilder 的总结

  1. 成员变量 count 记录数组位置,使用的是 int 类型,在多线程中未涉及到原子性,可能导致 count 的数值有误,从而导致最后输出的字符串有问题。
  2. 初始化的时候最好指定大小,避免触发 GC、以及数组之间的复制操作。

StringBuffer

也是提供三个构造方法。和 StringBuilder 没去。都是默认16,也可以自行初始化、或者直接写字符串。

不过在还新增了一个成员变量 toStringCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * A cache of the last value returned by toString. Cleared
 * whenever the StringBuffer is modified.
 */
private transient char[] toStringCache;


@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

根据 transient 的特性,我们可以知道这个 toStringCache 不会持久化,作为一个缓冲的存在。感觉这样做的原因是,这个值一直在改变,只有等到最后输出的时候才用到。可能节省空间吧。

另外除了构造方法,所有的方法都加上了 synchronized 修饰,来保证线程安全。所有的方法和 StringBuilder 差不多。

StringBuffer 总结

  1. 线程安全。
  2. 由于所有的方法都加上了 synchronized 所以效率是远满于 StringBuilder 的拼接速度。

总结

StringBuffer vs StringBuilder

线程安全 速度 操作业务场景
StringBuffer 安全 多线程
StringBuilder 不安全 很快 单线程


优化建议

  1. 尽可能的指定初始化大小,避免频繁的扩容、以及数组之间的复制,到而导致空置数组空间,触发 GC。
  2. 确定业务模型之后,是单线程或者业务并发不高,可以选择 StringBuilder,来拼接字符串。
  3. 高并发底下,请选择 StringBuffer 来拼接字符串。性能差可以接受,但是出问题,是不能容忍的。

声明

作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/09/29/jdk-StringBuilder-StringBuffer/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com