StringBuilder源码梳理1

        本文主要梳理了 JDK 8 中 StringBuilder 的无参构造方法、append(String str) 方法、setLength(int newLength) 方法的执行流程。

        知识点总结在最底下

1 类图

        StringBuilder 的 UML 类图如下,只记录了主要的方法、变量。

 

        StringBuilder 继承了 AbstractStringBuilder 抽象类,并实现了 java.io.Serializable、CharSequence 接口。StringBuilder 的大部分方法都是直接调用了父类 AbstractStringBuilder 中的方法,然后通过 return this 返回当前 StringBuilder 的实例。   

        AbstractStringBuilder 实现了 Appendable、CharSequence 接口,类中是一些核心的代码。成员变量有 value、count,value 是一个char[],用来保存拼接进来的字符;count 是 int 类型,用来记录“已使用的字符数量”。

2 构造方法

    public StringBuilder() {
        super(16);
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

      StringBuilder 的无参构造方法中会调用 AbstractStringBuilder 的 AbstractStringBuilder(int capacity) 构造方法,并且传参是 16,而 AbstractStringBuilder(int capacity) 构造方法中,会根据参数 capacity 给 成员变量 value 这个char[] 创建一个指定容量的字符数组。

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

        String(char value[], int offset, int count) 这个构造方法中,最终会执行数组的复制,创建一个长度为 count 的字符数组,并将原数组的元素复制到新数组中。

3 方法

        StringBuilder 的方法介绍按照方法所属的类分成 2 节介绍。

3.1 StringBuilder

3.1.1 append(String str)

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

        直接调用父类的 append(String str) 方法,并返回 this。

3.1.2 toString() 

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

        执行 String(char value[], int offset, int count) 构造方法,创建一个 String 对象。也就是将字符数组中已使用的这部分字符生成一个字符串。

3.2 AbstractStringBuilder

3.2.1 append(String str)

    public AbstractStringBuilder append(String str) {
        if (str == null)
            // 字符串为null,执行特定的 拼接null的方法
            return appendNull();
        int len = str.length();
        // 确保字符数组的容量足够放下新的字符串
        ensureCapacityInternal(count + len);
        // 将要拼接的字符串复制到字符数组中,从count位置开始往后拼len个
        str.getChars(0, len, value, count);
        // 更新 使用字符数量
        count += len;
        return this;
    }

        字符串为 null 会执行特殊的拼接 null 字符串的方法。在确保内部的字符数组容量充足后,会将字符串拼接到字符数组中,str.getChars(0, len, value, count) 涉及两个 char[] 的复制,注意复制的目标位置是从 count 下标开始,也就是从已使用的字符后面拼接

3.2.2 appendNull()

    private AbstractStringBuilder appendNull() {
        int c = count;
        // 确保字符数组的容量足够放下新的字符串
        ensureCapacityInternal(c + 4);
        // 拼上 n u l l 四个字符
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        // 更新 使用字符数量
        count = c;
        return this;
    }

         首先记录方法开始时的“使用字符数量”为 c,确保字符数组容量足够后,在下标 c 位置开始,往后偏移4个元素,分别赋值 'n' 'u' 'l' 'l' 四个字符。这里用 c 记录当前“使用字符数量”的快照,即使后续 count 发生变化,赋值操作也是基于开始时的“使用字符数量”来执行的

3.2.3 ensureCapacityInternal(int minimumCapacity)

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

         当所需要的容量大于字符数组的长度时,计算出一个新的容量,将原字符数组的元素复制到一个新的长度为新容量的数组中,并将 value 指向新的数组,也就是将字符数组扩容。Arrays.copyOf(char[] original, int newLength) 方法会创建一个新的char[],且数组长度为 newLength,而复制的偏移量只会取两数组中小的那个长度,防止索引越界

       为什么用 minimumCapacity - value.length > 0 来判断,而不直接用 minimumCapacity > value.length 判断?因为 minimumCapacity 是 int 类型,当数组长度特别大,超过 int 类型的最大值(0x7fffffff)时,会变成负数,直接用 minimumCapacity > value.length 判断布尔值会变成 false。

3.2.4 newCapacity(int minCapacity)

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        // 默认扩容后的容量
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            // 默认扩容后的容量依然不满足所需容量的话,就按照需要容量来
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

         这里可以看出,默认扩容机制是 增加1倍的容量再加2,使用位运算计算效率更高。当默认扩容的容量依然不满足要求的话,就按照所需要的容量扩容。然后还会判断新容量是否小于等于0 或者 大于 MAX_ARRAY_SIZE,是的话还会执行大容量处理的方法,注意这里传的参数是minCapacity,不是newCapacity

        这里的判断使用 newCapacity - minCapacity < 0 、newCapacity <= 0 、MAX_ARRAY_SIZE - newCapacity < 0 也是防止容量过大变成负数。MAX_ARRAY_SIZE 是 JVM 建议的数组可分配的最大长度

        当 value.length = 2147483647,拼接一个长度为2 ~ 2147483647的字符串时,minCapacity  = 2147483647 + 2 ~ 2147483647,默认扩容后 newCapacity = 0,此时 newLength - minCapacity < 0 都为 false,newCapacity 就是0, 所以才有 newCapacity <= 0 的判断。

3.2.5 hugeCapacity(int minCapacity)

    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

         当容量大于 int 类型最大值时,会抛内存不足错误。否则,如果所需容量大于 JVM 建议的数组可分配的最大长度,就返回需要的容量,不大于就返回可分配的最大数组长度。

        这里要结合 newCapacity(int minCapacity) 方法中调 hugeCapacity(int minCapacity) 方法传的参数为 minCapacity 理解,意思就是如果是用户主动拼接字符串导致的容量超过 MAX_ARRAY_SIZE,那么就按照用户的选择来;如果是程序默认扩容导致的容量超过MAX_ARRAY_SIZE,那么就最多分配 MAX_ARRAY_SIZE 的容量

        换句话说就是,如果你们报了OutOfMemoryError,这锅我们不背!

3.2.6 setLength(int newLength)

    public void setLength(int newLength) {
        if (newLength < 0)
            // 使用字符数量不能设置为负数
            throw new StringIndexOutOfBoundsException(newLength);

        // 确保容量足够
        ensureCapacityInternal(newLength);

        if (count < newLength) {
            // value 数组的 count 到 newLength - 1 的元素全部赋值为 '\0'
            Arrays.fill(value, count, newLength, '\0');
        }
        
        // 更新使用字符数量
        count = newLength;
    }

         当设置的新的“使用字符数量”为负数时,会抛字符串索引越界异常。在确保字符数组能容纳下新的字符数量后,如果新数量比原数量大,会用 '\0' 填充跳过去的这部分元素,再更新使用字符数量。

        在 Arrays.fill(value, count, newLength, '\0') 方法中,会先校验开始索引、偏移量在不在数组的范围内,在的话才会开始赋值。因为数组的长度是固定的,在调用 fill 方法的这一时刻,所要校验的数组就确定了,只要校验通过,赋值时不会索引越界。

4 总结

        经过上述的分析,整理一下知识点:

        1、new StringBuilder 若不指定容量,默认容量为16,本质上是 value = new char[16]。

        2、如果 append() 拼接的字符串为 null,会拼接字符串 "null", 不是空字符串("")。

        3、当所需容量大于 value 的长度,会扩容。默认扩容后的容量 = (原容量 × 2) + 2,若默认扩容不满足所需容量,按照所需容量扩容。

        4、巨大容量的处理:若所需容量大于 0x7fffffff,抛 OutOfMemoryError;若是程序默认扩容导致容量超 MAX_ARRAY_SIZE ,最大默认扩容容量限制为 MAX_ARRAY_SIZE。

        5、如果 setLength() 参数是负数,抛 StringIndexOutOfBoundsException。若参数比 count 大,将中间跳过的部分填充 '\0' 。若参数小于等于 count,只会更新 count。

        6、append() 拼接非 null 字符串,若不扩容,会导致 1次数组复制(偏移量为字符串长度);若扩容,则导致 1次 new char[newLength]、2 次数组复制(扩容复制的偏移量为原数组长度)。

        7、StringBuilder 的 toString() 方法,会创建 String 对象。只要 StringBuilder 的 count 大于 0,本质上是 1次 new char[count]、1次数组复制(偏移量为 count)。

(这里的数组复制指调用了 System.arraycopy() 本地方法,复制效率与偏移量有关)

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值