从JDK源码看StringBuilder

概况

在 Java 中处理字符串时经常会使用 String 类,实际上 String 对象的值是一个常量,一旦创建后不能被改变。正是因为其不可变,所以也无法进行修改操作,只有不断地 new 出新的 String 对象。

为此 Java 引入了可变字符串变量 StringBuilder 类,它不是线程安全的,只用在单线程场景下。

继承结构

类定义

StringBuilder 类被声明为 final,说明它不能再被继承。同时它继承了 AbstractStringBuilder 类,并实现了 Serializable 和 CharSequence 两个接口。

其中 Serializable 接口表明其可以序列化。

CharSequence 接口用来实现获取字符序列的相关信息,接口定义如下:

获取字符序列长度。

获取某个索引对应字符。

获取指定范围子字符串。

转成字符串对象。

用于获取字符序列的字符的 int 类型值的流,该接口提供了默认的实现。

用于获取字符序列的代码点的 int 类型的值的流,提供了默认的实现。

主要属性

value 该数组用于存储字符串值。

coder 表示该字符串对象所用的编码器。

count 表示该字符串对象中已使用的字符数。

构造方法

有若干种构造方法,可以指定容量大小参数,如果没有指定则构造方法默认创建容量为16的字符串对象。如果 COMPACT_STRINGS 为 true,即使用紧凑布局则使用 LATIN1 编码(ISO-8859-1编码),则开辟长度为16的 byte 数组。而如果是 UTF16 编码则开辟长度为32的 byte 数组。

如果构造函数传入的参数为 String 类型,则会开辟长度为的 byte 数组,并通过方法将字符串对象添加到 byte 数组中。

类似地,传入参数为 CharSequence 类型时也做相同处理。

主要方法

append方法

有多个方法,都只是传入的参数不同而已,下面挑几个典型的深入看看,其他都是类似的处理。

如果传入 String 类型参数则调用父类的方法将字符串对象添加到 StringBuilder 的 byte 数组中,然后返回 this。append 的逻辑为:

String 对象为 null的话则在 StringBuilder 的 byte 数组中添加 四个字符。

通过方法确保有足够的空间,如果没有则需要重新开辟空间。

通过方法将字符串对象里面的 byte 数组复制到 StringBuilder 的 byte 数组中,使用了进行复制。

count 为已使用的字符数,将其加上复制的字符串长度。

返回 this。

方法逻辑:

首先获取现有的容量大小。

如果需要的容量大于现有容量,则需要扩充容量,并且将原来的数组复制过来。

方法用于确定新容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小,另外,如果新容量大小为负则容量设置为,它的大小等于。

的逻辑:

String 对象的编码和 StringBuilder 对象的编码不相同,则先执行方法转换成 UTF16 编码。

如果 StringBuilder 对象不是 Latin1 编码则不执行转换。

通过扩充空间,因为UTF16编码的占位是 Latin1 编码的两倍。

通过将原来的值拷贝到扩充后的空间中。

通过将 String 对象的值拷贝到 StringBuilder 对象中。

传入的参数为 CharSequence 类型时,他会分几种情况处理,如果为空则添加 字符。另外还会根据对象实例化自 String 类型或 AbstractStringBuilder 类型调用对应的方法。

传入的参数为 char 数组类型时,逻辑如下:

通过方法确保足够容量。

append 过程中根据不同编码做不同处理。

如果是 Latin1 编码,从偏移量开始将一个个字符赋值到 StringBuilder 对象的字节数组中,这个过程中会检测每个字符是否可以使用 Latin1 编码来解码,可以的话则直接将 char 转成 byte 并进行赋值操作。否则为 UTF16 编码,此时先通过扩展空间,然后再通过将所有剩下的字符串以 UTF16 编码保存到 StringBuilder 对象中。

如果是 UTF16 编码,则直接通过将 char 数组添加到 StringBuilder 对象中。

修改 count 属性,即已使用的字节数。

传入的参数为 boolean 类型时,逻辑如下:

通过确定容量足够大,true 和 false 的长度分别为4和5。

如果为 Latin1 编码,按条件将 和 添加到 StringBuilder 对象的字节数组中。

如果为 UTF16 编码,则按照编码格式将 和 添加到 StringBuilder 对象的字节数组中。

如果传入的参数为 int 或 long 类型,则处理的大致逻辑都为先计算整数一共多少位数,然后再一个个放到 StringBuilder 对象的字节数组中。比如“789”,长度为3,对于 Latin1 编码则占3个字节,而 UTF16 编码占6个字节。

如果传入的参数为 float 或 double 类型,则处理的大致逻辑都为先计算浮点数一共多少位数,然后再一个个放到 StringBuilder 对象的字节数组中。比如“789.01”,长度为6,注意点也占空间,对于 Latin1 编码则占6个字节,而 UTF16 编码占12个字节。

appendCodePoint方法

该方法用于往 StringBuilder 对象中添加代码点。代码点是 unicode 编码给字符分配的唯一整数,unicode 有17个代码平面,其中的基本多语言平面(Basic Multilingual Plane,BMP)包含了主要常见的字符,其余平面叫做补充平面。

所以这里先通过判断是否属于 BMP 平面,如果属于该平面,此时只需要2个字节,则直接转成 char 类型并添加到 StringBuilder 对象。如果超出 BMP 平面,此时需要4个字节,分别用来保存 High-surrogate 和 Low-surrogate,通过完成获取对应4个字节并添加到 StringBuilder 对象中。

delete方法

该方法用于将指定范围的字符删掉,逻辑为:

end 不能大于已使用字符数 count,大于的话则令其等于 count。

通过检查范围合法性。

通过方法实现删除操作,其通过来实现,即把 end 后面的字符串复制到 start 位置,即相当于将中间的字符删掉。

修改已使用字符数 count 值。

返回 this。

deleteCharAt方法

删除指定索引字符,与 delete 方法实现一样,通过方法实现删除,修改 count 值。

replace方法

该方法用于将指定范围的字符替换成指定字符串。逻辑如下:

end 不能大于已使用字符数 count,大于的话则令其等于 count。

通过检查范围合法性。

计算新 count。

通过方法把 end 后面的字符串复制到 end + (newCount - count) 位置。

更新 count。

通过将字符串放到 start 后,直接覆盖掉后面的若干字符即可。

insert方法

该方法用于向 StringBuilder 对象中插入字符。根据传入的参数类型有若干个 insert 方法,操作都相似,深入看重点一个。

当传入的参数为 String 类型时,逻辑为:

通过检查偏移量的合法性。

如果字符串为空,则将字符串赋值给它。

通过确保足够的容量。

通过方法把 offset 后面的字符串复制到 offset+len 位置。

更新 count。

将 str 放到 offset 位置,完成插入操作。

返回 this。

除此之外,还可能插入 boolean 类型、object 类型、char 类型、char 数组类型、float 类型、double 类型、long 类型、int 类型和 CharSequence 类型。几乎都是先转成 String 类型再插入。

indexOf方法

该方法用于查找指定字符串的索引值,可以从头开始查找,也可以指定起始位置。

可以看到它间接调用了 String 类的方法,核心逻辑是如果是 Latin1 编码则通过查找,而如果是 UTF16 编码则通过查找。如果要查找的字符串编码和 StringBuilder 对象的编码不相同,则通过查找。

Latin1 编码的的主要逻辑为:先确定要查找的字符串的第一个字节 first,然后在 value 数组中遍历寻找等于 first 的字节,一旦找到等于第一个字节的元素,则比较剩下的字符串是否相等,如果所有都相等则查找到指定的字节数组,返回该索引值,否则返回-1。

UTF16 编码的逻辑与 Latin1 编码类似,只不过是需要两个字节合到一起(即比较 char 类型)进行比较。

另外如果源字符串的编码为 UTF16,而查找的字符串编码为 Latin1 编码, ,则通过来查找,查找逻辑也是类似,只不过需要把一个字节的 Latin1 编码转成两个字节的 UTF16 编码后再比较。

lastIndexOf方法

该方法用于从尾部开始反向查找指定字符串的索引值,可以从最末尾开始查找,也可以指定末尾位置。它的实现逻辑跟差不多,只是反过来查找,这里不再赘述。

reverse方法

该方法用于将字符串反转,实现逻辑如下,其实就是做一个反转操作,遍历整个 StringBuilder 对象的数组,实现反转。其中分为 LATIN1 编码和 UTF16 编码做不同处理。

toString方法

该方法用于返回 String 对象,根据不同的编码分别 new 出 String 对象。其中 UTF16 编码会尝试压缩成 LATIN1 编码,失败的话则以 UTF16 编码生成 String 对象。

writeObject方法

该方法是序列化方法,先按默认机制将对象写入,然后再将 count 和 char 数组写入。

readObject方法

该方法是反序列方法,先按默认机制读取对象,再读取 count 和 char 数组,最后再初始化对象内的字节数组和编码标识。

--------------------------------------

跟我交流,向我提问:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180525G0825G00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券