当前位置:   article > 正文

【jdk源码阅读】第三篇:StringBuffer、StringBuilder源码分析

【jdk源码阅读】第三篇:StringBuffer、StringBuilder源码分析

String虽然是线程安全的,但是在某些场合的表现不尽如人意,上篇文章也分析了,String底层的数据存储借助了final修饰的字符数组,一旦创建便不能修改。

“宏观”上的字符串截取、拼接等操作,都是通过创建新的对象实现的,涉及大量的数组拷贝,性能不佳。

StringBuffer和StringBuilder底层也是字符数组,但是并没有用final修饰,是可变的。

在考虑线程安全的同时,我们也要去了解其内部的存储机制、扩容机制,以及StringBuffer中的二级缓存。

继承体系

StringBuilder与StringBuffer处于同一层级,就不重复贴图了。

CharSequence和Serializable都是老朋友了,我们看下Appendable接口

Appendable主要是为字符序列(有序字符的集合)拼接规范接口。

AbstractStringBuilder

一个抽象类居然独占一个小标题?

因为这个抽象类是一个重量级的抽象类,只有一个抽象方法,其他所有方法都有对应的实现。

  1. @Override
  2. public abstract String toString();

就算不看源码,我们也应该知道StringBuffer和StringBuilder主要的区别在于是否线程安全。

在实现的功能上、包括对外开放的接口都是一样的,只是StringBuffer可能会对某些共享资源或者某些方法做一些访问控制。

除此之外,StringBuffer为了提高重复调用toString方法时得性能,提供了一个“二级缓存”,toString()方法与StringBuilder的实现不太一致,否则我觉得这俩兄弟甚至都可以共同继承一个实现全功能的类了(而不仅仅是抽象类)。

 

下面先分析一下AbstractStringBuilder这个类,包含一些字符串拼装逻辑、以及关键的buffer扩容机制等。

成员变量

  1. /**
  2. * The value is used for character storage.
  3. */
  4. char[] value;
  5. /**
  6. * The count is the number of characters used.
  7. */
  8. int count;

value用于存储字符序列,count用于记录字符的长度,这些变量主要是为子类服务的,为了保证对其子类的可见性,没有定义为private。

扩容机制

虽然定义了字符数组,但并没有申请空间,为了保证Buffer的正常使用,必然会在构造函数执行期间执行初始化操作。

  1. AbstractStringBuilder(int capacity) {
  2. value = new char[capacity];
  3. }

如果容量不够,则需要扩容。
先看下面的两个函数

  1. public void ensureCapacity(int minimumCapacity) {
  2. if (minimumCapacity > 0)
  3. ensureCapacityInternal(minimumCapacity);
  4. }
  5. private void ensureCapacityInternal(int minimumCapacity) {
  6. // overflow-conscious code
  7. if (minimumCapacity - value.length > 0) {
  8. value = Arrays.copyOf(value,
  9. newCapacity(minimumCapacity));
  10. }
  11. }

传入一个最保守的期望值,如果当前数组长度不满足需求,则会扩容。

扩容时,会将已有的数据拷贝到一个新的数组,新数组的长度由newCapacity方法决定。

  1. private int newCapacity(int minCapacity) {
  2. // overflow-conscious code
  3. int newCapacity = (value.length << 1) + 2;
  4. if (newCapacity - minCapacity < 0) {
  5. newCapacity = minCapacity;
  6. }
  7. return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
  8. ? hugeCapacity(minCapacity)
  9. : newCapacity;
  10. }
  11. private int hugeCapacity(int minCapacity) {
  12. if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
  13. throw new OutOfMemoryError();
  14. }
  15. return (minCapacity > MAX_ARRAY_SIZE)
  16. ? minCapacity : MAX_ARRAY_SIZE;
  17. }

int newCapacity = (value.length << 1) + 2;    左移可以使原来的数扩大2的n次方倍

新数组长度 =  现有数组的长度 *  2   +  2

方法的最后,对于边界值做了一点处理。

不知道有没有人有同样的疑问,为什么会加 newCapacity < 0这个条件?

因为移位运算是有溢出风险的。(代码中也标出来了, overflow-conscious code)

举个栗子

  1. public class StringBuilderMain {
  2. public static void main(String[] args) {
  3. int newCapacity = ((0x7fffffff - 100) << 1) + 2;
  4. System.out.println(newCapacity);
  5. }
  6. }

我们取比Integer最大值小200的数,得到的结果

确实是负数。

注:

0x7fffffff = Integer.MAX_VALUE

 

AbstractStringBuilder中定义的数组最大值为

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

如果扩容时,想要指定一个更大的值,会直接抛出OOM异常。

扩容时机

当需要向数组里填充数据时,就需要检查数组的剩余的空间是否满足需求了。

以append(String)方法为例

StringBuilder

基本实现

jdk1.5以后才有的StringBuilder 。

在父类的基础上,用上了建造者模式,大部分方法形如:

在父类的基础上,并没有做什么加工。

构造期间,给字符数组指定的初始长度为16

重写了toString方法

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

点进String的构造函数,应该可以发现,本质也是数组拷贝(注意这里与StringBuffer的区别)

多线程环境

StringBuilder中的共享资源继承自父类 

value数组与统计字符数量的count变量都是可更改的,没有做访问控制(是不安全的)。

StringBuffer

StringBuffer不仅继承了父类的char数组,还定义了额外的数组做缓存。(transient表示不参与序列化,毕竟这只是个缓存,仅在本地使用)

  1. /**
  2. * A cache of the last value returned by toString. Cleared
  3. * whenever the StringBuffer is modified.
  4. */
  5. private transient char[] toStringCache;

当数组的值发生更改时,会清除缓存

同时我们可以发现,与共享变量相关的方法,都加了锁,在多线程环境下可以放心使用。

缓存数组一直在被清空,直到调用toString()方法,才会用上缓存。

  1. @Override
  2. public synchronized String toString() {
  3. if (toStringCache == null) {
  4. toStringCache = Arrays.copyOfRange(value, 0, count);
  5. }
  6. return new String(toStringCache, true);
  7. }

我们刚才看StringBuilder的源码,发现toString()方法返回字符串时,依然进行了数组拷贝。

而StringBuffer这边做了优化,如果重复调用toString方法,并且中间没有修改过Buffer,那么只需要进行一次数组拷贝即可。

我们进入String的构造函数

  1. String(char[] value, boolean share) {
  2. // assert share : "unshared not supported";
  3. this.value = value;
  4. }

发现它给StringBuffer开了捷径,直接引用复制,没有数组拷贝。

其实这么做是相当危险的,为了说明这个问题,我自定义一个String

  1. public class MyString {
  2. private final char[]arr;
  3. public MyString(char[]arr) {
  4. this.arr = arr;
  5. }
  6. public void print() {
  7. for (char c : arr) {
  8. System.out.print(c + "\t");
  9. }
  10. System.out.println("\n");
  11. }
  12. public static void main(String[] args) {
  13. char[] chars = new char[]{'a', 'b'};
  14. MyString myString = new MyString(chars);
  15. myString.print();
  16. chars[0] = 'c';
  17. myString.print();
  18. }
  19. }

输出结果

final修饰的数组,如果通过引用赋值来初始化自己,是不安全的,因为可以通过别的引用(代码中chars数组)可以修改数组的值。

AbstractStringBuilder维护的数组是一直在变化的,不可直接给String中的char数组引用赋值,所以StringBuilder中的toString()方法才会不厌其烦地进行数组拷贝。

StringBuffer为了线程安全,方法加了锁,性能上已经做了牺牲,所以jdk给他又准备了一个缓存。

private transient char[] toStringCache;

与这个缓存的代码,其实只有两种形式

第一种:置为null

toStringCache = null;

第二种:数组拷贝式赋值

  1. @Override
  2. public synchronized String toString() {
  3. if (toStringCache == null) {
  4. toStringCache = Arrays.copyOfRange(value, 0, count);
  5. }
  6. return new String(toStringCache, true);
  7. }

不会通过toStringCache这个引用去修改数组的值,所以这个二级缓存是安全的,可以放心的交给String。

String也很给面子,敲敲开了后门。大伙应该注意到了吧,String的这个构造函数没有用public修饰,所以只能同包的类使用(java.lang),我们在开发过程中是不能直接用这个构造函数的。

所以说,Java是真的“安全”!

 

才疏学浅,如有错误,欢迎批评指正!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/1020270
推荐阅读
相关标签
  

闽ICP备14008679号