StringBuilder类的数据结构和扩容方式解读
目录
StringBuilder是什么
核心特性:
StringBuilder数据结构
1. 核心存储结构(基于父类 AbstractStringBuilder)
2. 类定义与继承关系
3. 数据结构的核心特点
StringBuilder数据结构的初始化方式
1. 无参构造:默认初始容量为 16
2. 指定初始容量:自定义数组大小
3. 基于已有字符串初始化:容量 = 字符串长度 + 16
初始化的核心逻辑
在append()追加时,如果容量不足,如何扩容
1. 计算所需的最小容量
2. 判断是否需要扩容
3. 确定新的容量
4. 创建新数组并复制数据
5. 更新相关属性
源码解析如下:
深入理解 String 类的不可变性及其内部数据结构_Java字符串拼接性能优化-CSDN博客
小编在这篇博客中讲过,当我们对一个String
对象进行修改(如拼接、替换等)时,原对象的内容不会被改变,JVM 会创建一个全新的String
对象来存储修改后的结果,并将变量引用指向新对象。
String类型的不可变保障线程安全,优化哈希性能,支持常量池复用以及确保数据安全,但在需要频繁修改字符串的场景中(例如循环拼接大量数据),会产生大量临时对象,不仅占用额外内存,还会增加垃圾回收的负担,导致性能显著下降。而StringBuilder的出现弥补了String类这一缺点,今天,我们就来深入解读StringBuilder
类的数据结构与扩容方式,看看它是如何高效应对字符串的动态变化的。
StringBuilder是什么
StringBuilder
是 Java 中用于处理可变字符序列的类,主要用于高效地进行字符串的动态构建和修改,是为解决 String
类在频繁字符串操作时的性能问题而设计的。
核心特性:
-
可变性
这是StringBuilder
最核心的特性。与String
的不可变性不同,StringBuilder
的内部字符序列可以直接修改(如拼接、插入、删除等),所有操作都在原有对象上进行,不会像String
那样每次修改都创建新对象,从而大幅减少内存消耗和垃圾回收压力。 -
高效的字符串操作
提供了丰富的操作方法,如append()
(追加任意类型数据)、insert()
(指定位置插入)、delete()
(删除区间字符)、replace()
(替换字符)等,这些方法均针对性能优化设计,执行效率远高于String
的同类操作(尤其在循环拼接等场景中)。 -
非线程安全
StringBuilder
的方法没有添加synchronized
同步锁,因此在多线程环境下并发修改可能导致数据不一致。但这也使其在单线程场景中性能优于线程安全的StringBuffer
(避免了同步带来的开销)。 -
动态扩容机制
内部基于字符数组(Java 9 后优化为字节数组,根据字符编码动态调整存储方式)存储数据,初始容量可自定义(默认 16)。当追加内容导致容量不足时,会自动扩容(通常为原容量的 2 倍加 2),并复制原有数据到新数组,平衡了内存占用和操作效率。 -
与 String 的便捷转换
可通过toString()
方法快速转换为String
对象,满足需要String
类型参数的场景,转换过程会创建新的String
对象,但仅执行一次,性能开销可控。
// 创建对象
StringBuilder sb = new StringBuilder();// 追加内容
sb.append("Hello");
sb.append(" ");
sb.append("World");// 插入内容
sb.insert(5, ","); // 在索引5处插入逗号// 转换为String
String result = sb.toString(); // 结果:"Hello, World"
StringBuilder数据结构
1. 核心存储结构(基于父类 AbstractStringBuilder
)
StringBuilder
本身不直接定义存储结构,而是继承自抽象类 AbstractStringBuilder
,核心数据存储由父类维护,主要包含两个关键成员变量:
-
存储字符的数组:
- Java 8 及之前:使用
char[] value
数组,每个字符固定占用 2 个字节(UTF-16 编码),直接存储字符序列。 - Java 9 及之后:优化为
byte[] value
数组,根据字符类型动态选择编码:- 若字符为 ASCII 或 Latin-1 范围内(单字节可表示),则用 1 字节存储,节省内存;
- 若包含多字节字符(如中文、日文),则用 2 字节存储(UTF-16 编码)。
- Java 8 及之前:使用
-
字符长度计数器:
int count
,记录当前存储的有效字符数量(而非数组容量)。
2. 类定义与继承关系
StringBuilder
的类定义简化如下(省略部分接口实现):
// 继承 AbstractStringBuilder,实现字符序列和可追加接口
public final class StringBuilder extends AbstractStringBuilder implements CharSequence, Appendable {// 构造方法(调用父类构造)public StringBuilder() {super(16); // 默认初始容量 16}public StringBuilder(int capacity) {super(capacity); // 指定初始容量}public StringBuilder(String str) {super(str.length() + 16); // 初始容量 = 字符串长度 + 16append(str);}// 其他方法(如 append、insert 等,实际调用父类方法)
}// 父类 AbstractStringBuilder 的核心定义
abstract class AbstractStringBuilder implements Appendable, CharSequence {// Java 9+ 用 byte[],Java 8 及之前用 char[]byte[] value; int count; // 有效字符数// 父类构造方法(初始化数组容量)AbstractStringBuilder(int capacity) {if (capacity < 0)throw new NegativeArraySizeException();value = new byte[capacity]; // 初始化数组}
}
3. 数据结构的核心特点
- 数组存储:通过连续的数组结构存储字符,支持快速随机访问(如通过索引获取字符)。
- 容量与长度分离:
value
数组的长度是「容量」(可容纳的最大字符数),count
是「实际长度」(已存储的字符数),容量 ≥ 长度。 - 动态适配:Java 9+ 的
byte[]
优化减少了单字节字符的内存占用,平衡了存储效率和兼容性。
StringBuilder
数据结构的初始化方式
StringBuilder
内部数据结构(字符数组)的初始化方式由其构造方法决定,主要通过以下 3 种方式指定初始容量,以适配不同场景的字符串操作需求:
1. 无参构造:默认初始容量为 16
当使用无参构造方法 new StringBuilder()
时,会调用父类 AbstractStringBuilder
的构造方法,初始化一个容量为 16 的字符数组(Java 8 及之前为 char[]
,Java 9+ 为 byte[]
)。
StringBuilder sb = new StringBuilder();
// 内部数组初始容量为 16,可直接存储 16 个字符(或更多,取决于编码)
2. 指定初始容量:自定义数组大小
如果已知字符串的大致长度,可通过 new StringBuilder(int capacity)
手动指定初始容量,避免后续频繁扩容。父类会根据传入的 capacity
初始化对应大小的数组。
StringBuilder sb = new StringBuilder(100);
// 内部数组初始容量为 100,适合预计存储约 100 个字符的场景
3. 基于已有字符串初始化:容量 = 字符串长度 + 16
当传入一个 String
对象作为参数(new StringBuilder(String str)
)时,初始容量会设为「字符串长度 + 16」,既容纳原有字符串,又预留 16 个字符的空间供后续追加。同时会将传入的字符串内容复制到内部数组中。
String str = "hello"; // 长度为 5
StringBuilder sb = new StringBuilder(str);
// 内部数组初始容量 = 5 + 16 = 21,且已存储 "hello" 这 5 个字符
初始化的核心逻辑
无论哪种方式,最终都是通过父类 AbstractStringBuilder
的构造方法完成数组初始化:
// 父类 AbstractStringBuilder 的构造方法(简化)
AbstractStringBuilder(int capacity) {if (capacity < 0) {throw new NegativeArraySizeException();}// 初始化数组(Java 8 为 char[],Java 9+ 为 byte[])value = new byte[capacity]; // 以 Java 9+ 为例
}
通过合理选择初始化方式(尤其是指定初始容量),可以减少后续 append()
操作时的扩容次数,进一步提升 StringBuilder
的性能。这种结构设计使得 StringBuilder
既能直接修改原有数据(实现可变性),又能通过数组的连续内存特性保证操作效率,是其高性能的核心基础。
在append()追加时,如果容量不足,如何扩容
当使用 StringBuilder
的 append()
方法追加字符或字符串时,如果当前内部字符数组的剩余容量不足以容纳新的数据,就会触发扩容操作。StringBuilder
扩容的具体流程如下(以 Java 8 及之后版本为例,Java 9 后对存储方式有优化,但扩容逻辑主体一致 ):
1. 计算所需的最小容量
在追加数据之前,StringBuilder
会先计算追加新数据后总共需要的字符数量,将其记为 minCapacity
。这个值是在原有有效字符数量(由 count
变量记录)的基础上,加上即将追加的数据的字符长度。
2. 判断是否需要扩容
将计算得到的 minCapacity
与当前字符数组(value
)的容量(即 value.length
)进行比较。如果 minCapacity
大于当前数组的容量,就意味着当前数组无法容纳新追加的数据,此时需要进行扩容操作。
3. 确定新的容量
StringBuilder
采用一种简单的扩容策略来确定新的数组容量,具体的计算方式是将当前容量乘以 2 再加 2 ,即
newCapacity = oldCapacity * 2 + 2
例如,如果当前数组的容量 oldCapacity
为 16,那么扩容后的新容量 newCapacity
就是 16 * 2 + 2 = 34
。
不过,如果通过上述计算得到的 newCapacity
仍然小于 minCapacity
,则会直接将 newCapacity
设置为 minCapacity
,以确保新数组能够容纳追加后的数据。
4. 创建新数组并复制数据
确定了新的容量 newCapacity
后,StringBuilder
会创建一个新的字符数组(char[]
),其长度为 newCapacity
。然后,通过系统调用(System.arraycopy()
)将原数组中的数据复制到新数组中。这一步保证了原有的字符序列不会丢失,并且新数组有足够的空间来存储即将追加的数据。
5. 更新相关属性
复制完成后,StringBuilder
会将内部用于存储字符的数组引用(value
)指向新创建的数组,同时更新其他可能相关的属性(例如在一些优化的实现中,可能会涉及到对编码标识等属性的调整 ),使得后续的追加操作可以在新的、更大容量的数组上进行。
源码解析如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence {// 追加字符的方法public AbstractStringBuilder append(char c) {ensureCapacityInternal(count + 1);putChar(count++, c);return this;}// 追加字符串的方法public AbstractStringBuilder append(String str) {if (str == null)return appendNull();int len = str.length();ensureCapacityInternal(count + len);str.getBytes(coder, 0, len, value, count);count += len;return this;}// 确保内部容量足够的方法private void ensureCapacityInternal(int minimumCapacity) {// overflow-conscious codeif (minimumCapacity - value.length > 0)expandCapacity(minimumCapacity);}// 执行扩容的方法void expandCapacity(int minimumCapacity) {int newCapacity = value.length * 2 + 2;if (newCapacity - minimumCapacity < 0)newCapacity = minimumCapacity;if (newCapacity < 0) {if (minimumCapacity < 0) // overflowthrow new OutOfMemoryError();newCapacity = Integer.MAX_VALUE;}value = Arrays.copyOf(value, newCapacity);}
}
append(char c)
和append(String str)
方法在追加数据前,都会先调用ensureCapacityInternal(int minimumCapacity)
方法,计算追加数据后所需的最小容量minimumCapacity
,并判断当前数组容量是否足够。- 如果当前容量不足,
ensureCapacityInternal
方法会调用expandCapacity(int minimumCapacity)
进行扩容。expandCapacity
方法先按照newCapacity = oldCapacity * 2 + 2
的规则计算新容量newCapacity
,如果计算出的新容量小于minimumCapacity
,则将newCapacity
设置为minimumCapacity
。同时,还会处理新容量溢出的情况,若溢出且minimumCapacity
小于 0,抛出OutOfMemoryError
,否则将newCapacity
设置为Integer.MAX_VALUE
。最后通过Arrays.copyOf
方法将原数组数据复制到新的、更大容量的数组中,完成扩容。
通过以上步骤,StringBuilder
实现了在容量不足时的自动扩容,从而保证了在频繁追加数据的情况下,仍然能够高效地进行字符串操作。
StringBuilder
凭借其可变的字符序列设计、灵活的初始化方式以及智能的扩容机制,完美解决了String
在频繁字符串操作时的性能瓶颈。从数据结构到扩容逻辑,每一处都为高效处理动态字符串而生,是 Java 中构建复杂字符串、提升程序性能的得力工具,尤其在大量字符串拼接等场景下,能显著优化内存使用与执行效率。
有问题欢迎留言!!!😗
肥嘟嘟左卫门就讲到这里啦,记得一键三连!!!😗