jdk 源码系列之StringBuilder、StringBuffer
前言
StringBuilder、StringBuffer 经常使用到,分析 StringBuilder、StringBuffer 源码、通过对比加深对这两个类的了解,以及以后更好的使用。
父类
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 ,这说明 StringBuilder 、 StringBuffer 他们都是 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 的总结
- 成员变量 count 记录数组位置,使用的是 int 类型,在多线程中未涉及到原子性,可能导致 count 的数值有误,从而导致最后输出的字符串有问题。
- 初始化的时候最好指定大小,避免触发 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 总结
- 线程安全。
- 由于所有的方法都加上了 synchronized 所以效率是远满于 StringBuilder 的拼接速度。
总结
StringBuffer vs StringBuilder
类 | 线程安全 | 速度 | 操作业务场景 |
---|---|---|---|
StringBuffer | 安全 | 慢 | 多线程 |
StringBuilder | 不安全 | 很快 | 单线程 |
优化建议
- 尽可能的指定初始化大小,避免频繁的扩容、以及数组之间的复制,到而导致空置数组空间,触发 GC。
- 确定业务模型之后,是单线程或者业务并发不高,可以选择 StringBuilder,来拼接字符串。
- 高并发底下,请选择 StringBuffer 来拼接字符串。性能差可以接受,但是出问题,是不能容忍的。
声明
作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/09/29/jdk-StringBuilder-StringBuffer/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com