StringBuilder和StringBuffer是 Java 中用于处理字符串的两个类,它们的底层存储原理和使用方法基本类似,但有一些关键的区别。
一、StringBuilder 和 StringBuffer 的共同点
1.可变性:两者都提供了可变的字符串存储,允许动态修改字符串内容,而不需要每次修改都创建一个新的对象。
2.字符数组存储:它们的底层实现都是通过一个字符数组 char[] 来存储字符串内容。
二、主要区别
1.线程安全性:
StringBuilder:非线程安全的。适合在单线程环境下使用,因为其方法没有进行同步,所以效率较高。
StringBuffer:线程安全的。通过在每个方法上加上 synchronized 关键字来保证线程安全,因此可以安全地在多线程环境下使用,但相对于StringBuilder会有一定的性能损耗。
2.性能比较:
在单线程环境下,StringBuildr 的性能优于StringBuffer,因为不需要进行线程同步操作。
在多线程环境下,StringBuffer由于需要进行同步操作,可能会比StringBuilder略慢。
三、底层存储实现
无论是StringBuilder还是StringBuffer,它们的底层存储实现机制基本相同:
1.char[] 数组:用于存储字符串的字符序列。
2.扩容策略:当字符串长度不足以容纳新添加的字符时,会自动扩展数组的大小。通常是通过创建一个新的更大的数组,并将旧数组的内容复制到新数组来实现的。
四、扩容策略
从底层源码角度来说,StringBuilder和StringBuffer的扩容策略确实略有差异,尽管它们的整体思路相似。以下是它们的扩容策略的详细解释。
1.StringBuilder 的扩容策略
StringBuilder的扩容策略可以在 OpenJDK 的源码中找到具体的实现。以下是StringBuilder在 Java 8 版本中的扩容策略:
1.初始容量和最大容量:
(1)StringBuilder的默认初始容量是 16 个字符。
(2)最大容量是Integer.MAX_VALUE - 8,因为需要保留一些空间用于对象头信息和其他一些标记。
下面是一个例子,给StringBuilder对象中添加26个英文字母。
public class Test {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (char i = 'a'; i < 'z'; i++) {
// append(),追加元素到末尾。
sb.append(i);
}
System.out.println(sb);
}
}
当我们用无参构造创建一个StringBuilder对象sb时,发现在StringBuilder类中的无参构造调用了父类AbstractStringBuilder的有参构造(参数为16)。
进入到父类AbstractStringBuilder的的有参构造中,发现将参数16赋给了一个char类型的数组value,这就是StringBuilder对象的默认初始容量(16个字符)。
但是,英文字母有26个,已经超过了StringBuilder的默认初始容量,剩下的元素改如何添加呢?这就涉及到了StringBuilder的扩容操作。
2.扩容操作:
(1)如果需要扩容,会创建一个新的字符数组,并将原有的字符数组内容复制到新数组中。
(2)扩容操作是通过Arrays.copyOf方法来实现的,这个方法会创建一个新的数组并复制指定长度的内容。
接着,让我们进入到append方法中。
append方法中调用了ensureCapacityInternal方法,再进入到ensureCapacityInternal方法中。
我们发现,如果所需的最小容量大于value的长度,就进入到if判断内,可以看出扩容操作是通过Arrays.copyOf方法来实现的,这个方法会创建一个新的数组并复制指定长度的内容。
Arrays.copyOf方法第二个参数调用了一个名为newCapacity的方法,传入的参数为所需的最小容量。
让我们再进入到这个newCapacity方法中,可以发现此处定义了一个局部变量newCapacity,它的长度等于value的长度×2在+2,这就是扩容后的StringBuilder对象的容量。
int newCapacity = (value.length << 1) + 2;
如果所需的最小容量大于扩容后的容量,那么再将最小容量赋给扩容后的容量。
最后再返回局部变量newCapacity,这就是最终扩容后的StringBuilder对象的容量。
这里,我们也可以看到StringBuilder的最大容量是Integer.MAX_VALUE - 8。
3.性能考虑:
(1)扩容操作的性能是可预测的,因为每次扩容都是按照固定的公式进行的,不会出现不必要的内存浪费。
2.StringBuffer 的扩容策略
StringBuffer的扩容策略与StringBuilder类似,但有一个主要区别在于线程安全的考虑:
1.初始容量和最大容量:
(1)StringBuffer的默认初始容量也是 16 个字符。
(2)最大容量也是Integer.MAX_VALUE - 8,与StringBuilder相同。
依旧是上面那个例子,不过这次创建的是StringBuffer的对象。
public class Test {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (char i = 'a'; i < 'z'; i++) {
// append(),追加元素到末尾。
sb.append(i);
}
System.out.println(sb);
}
}
首先调用StringBuffer的无参构造创建一个对象,在Stringbuffer无参构造中调用了父类AbstractStringBuilder的有参构造,传入参数16。
进入到父类AbstractStringBuilder的有参构造中可以看到将参数16赋给了一个char类型的数组value,这就是StringBuffer对象的默认初始容量(16个字符)。
同样的,当我们所需的最小容量超过StringBuffer的默认容量16个字符时,StringBuffer将实行扩容操作。
2.扩容操作:
(1)与StringBuilder类似,如果需要扩容,会创建一个新的字符数组,并将原有的字符数组内容复制到新数组中。
接下来让我们进入到append方法中,可以发现append方法被synchronized关键字来修饰,这就是StringBuffer的线程安全处理。
线程安全处理:StringBuffer在进行扩容时,使用了synchronized关键字来保证线程安全,确保在多线程环境下不会出现竞态条件。
后面的操作就和StringBuilder相同了,这里不做过多介绍。
newCapacity方法中定义了一个局部变量newCapacity,它的长度等于value的长度×2在+2,这就是扩容后的StringBufferr对象的容量。
int newCapacity = (value.length << 1) + 2;
如果所需的最小容量大于扩容后的容量,那么再将最小容量赋给扩容后的容量。
最后再返回局部变量newCapacity,这就是最终扩容后的StringBuffer对象的容量。
这里可以看到StringBuffer的最大容量也是Integer.MAX_VALUE - 8。
3.性能考虑:
(1)由于涉及线程同步,StringBuffer在多线程环境下的性能可能略低于StringBuilder,因为需要考虑线程安全带来的额外开销。
补充:Integer.MAX_VALUE的值为:
3.总结
StringBuilder和StringBuffer的扩容策略都是通过动态调整字符数组的大小来实现的,保证了字符串操作的高效性和可扩展性。理解它们的扩容机制有助于我们编写出性能更优的字符串处理代码,在应对不同需求时选择合适的类进行使用。
这种设计保证了在大部分情况下,字符串操作的性能是高效的,因为不需要频繁地重新分配和复制数组,而只在必要时进行扩容操作。
五、综上
综上所述,StringBuilder和StringBuffer虽然在线程安全性和性能上有所差异,但它们的底层存储原理和扩容策略基本相同,都是通过字符数组来存储字符串,并且通过动态扩展数组来支持字符串的动态修改。