Java 深入理解StringBuilder和StringBuffer 自动扩容分析

Java 笔记 专栏收录该内容
8 篇文章 0 订阅

一、StringBuilder

1.继承关系

StringBuilder继承关系图
可见StringBuilder直接继承至AbstractStringBuilder

2.构造器

1)StringBuilder的构造器

public StringBuilder() {
        super(16);
    }
    
public StringBuilder(int capacity) {
        super(capacity);
    }
    
public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

这些构造器都调用了父类(即AbstractStringBuilder类)的构造器,我们看一下AbstractStringBuilder的构造器

2)AbstractStringBulider的构造器

char[] value;//用于字符存储
int count;//已使用字符数

AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

当调用这个构造器时,会生成容量为capacity的char数组,如果StringBuilder没有传入capacity,则会调用super(16),即默认构造一个容量为16的字符数组

3.append方法(自动扩容原理)

虽然我们一开始指定了字符数组value的容量,但StringBuilder构造的对象是一个可变对象,调用append方法会占用容量,当容量不足时,会自动扩容
1)StringBuilder的append方法

@Override
public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

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

...省略部分重载方法

StringBuilder的append方法有很多重载方法,但这些重载方法又都共同的调用了父类(AbstractStringBuilder)的append方法

2)AbstractStringBuilder的append方法

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

a.AbstractStringBuilder的append方法也有很多重载方法,但大都用的相同的方法内容。
b.首先会判断传进来的参数是不是null,如果是则直接调用appendNull方法,将null这四个字符添加到value字符数组中,并且已使用字符数count要加4
c.如果传进来参数不是null,则调用ensureCapacityInternal判断字符数组value的容量是否足够,其中,count为已经使用的字符数(容量),len为将要添加的字符数,总共就是目前需要的容量,下面我们看一下ensureCapacityInternal的源码

appendNull方法内部也会调用ensureCapacityInternal判断加上null这4个字符长度是否会超过value数组规定的容量

3)AbstractStringBuilder 的ensureCapacityInternal方法

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

a.如果传进来的参数小于value数组的长度,那么说明容量已经足够,不需要扩容
b.如果传进来的参数大于value数组的长度,说明原本设定的容量已经不够用,需要扩容处理,这里的Arrays.copyOf(char[] a,int b),就是将a这个数组复制到一个b长度的新数组中,并返回这个b长度的数组,这里的b就是newCapacity(minimumCapacity),看一下其源码

4)AbstractStringBuilder 的newCapacity方法

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;
    }

a.上一步已经说明了,调用newCapacity说明需要扩容,这里首先将原来value数组的长度左移1位(即乘2)再加上2,并赋值给一个新变量newCapacity,代表扩容后的长度
b.因为这样扩容的数组容量可能还是不够,所以再将目前需要的容量minCapacity扩容后的容量newCapacity相比较,取其中最大者赋值给newCapacity
c.最后,赋值后的newCapacity还要与MAX_ARRAY_SIZE相比较,它代表要分配数组的最大大小,它的值为Integer.MAX_VALUE - 8,如果newCapacity超过了这个值,那么会调用hugeCapacity方法,这个方法可能会抛出OutOfMemory错误;如果newCapacity没超过MAX_ARRAY_SIZE,那么会返回newCapacity,hugeCapacity方法源码如下:

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

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

5)上面的第3)步即判断是否扩容,第4)步实现扩容,完成后,我们再回到第2)步,接下来执行str.getChars(0, len, value, count),内部调用了System.arraycopy,作用是从value字符数组(此时的value字符数组已经完成了扩容判断)的count位置上开始复制拼接传进来的str,拼接的长度即为len-0(即str的长度)

6)最后将已经使用的字符数count的值加上传进来的str的长度值,并返回当前对象,此时append方法调用完成

7)补充:
append方法中的扩容过程中常用思路就是将原来的数组copy到一个新的数组中,其中使用的复制方法基本都是调用底层的System.arraycopy方法:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

其中:

  • src:源数组(被拷贝的数组)
  • srcPos:在源数组中的拷贝起点
  • dest:目标数组
  • destPost:拷贝到目标数组中的起点
  • length:要拷贝的数组元素个数

举例:

char[] arr1 = {'a', 'b', 'c'};
char[] arr2 = {'d', 'e', 'f', 'g'};
System.arraycopy(arr1, 0, arr2, 1, arr1.length);
System.out.println(Arrays.toString(arr1) +"\n"+ Arrays.toString(arr2));
/**output
[a, b, c]
[d, a, b, c]
**/

注意这里的拷贝是浅拷贝

4.其他方法

1)length方法

@Override
public int length() {
    return count;
}

StringBuilder中没有length方法,但可以直接调用父类的length方法,返回已经使用过的字符数

2)capacity方法

public int capacity() {
 return value.length;
}

也是AbstractStringBuilder类中的方法,返回的是构造的字符数组value的容量

3)insert方法
调用的是父类的insert方法::

public AbstractStringBuilder insert(int offset, String str) {
        if ((offset < 0) || (offset > length()))
            throw new StringIndexOutOfBoundsException(offset);
        if (str == null)
            str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        System.arraycopy(value, offset, value, offset + len, count - offset);
        str.getChars(value, offset);
        count += len;
        return this;
    }
 ...省略其他重载方法

跟append方法很相似,先判断offset偏移量是否正确以及插入对象是不是null,然后进行扩容判断,最后进行数组拷贝,更新长度,返回this对象

4)delete方法
调用的父类的方法:

public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            end = count;
        if (start > end)
            throw new StringIndexOutOfBoundsException();
        int len = end - start;
        if (len > 0) {
            System.arraycopy(value, start+len, value, start, count-end);
            count -= len;
        }
        return this;
    }

注意:这里delete后,原来的value数组里面可能还是会有已经删除的值,但是调用toString时不会输出出来,因为StirngBuilder里的内容长度是由count决定的,我们最后输出的时候只是输出了count长度的内容
举个例子:我们创建一个StringBuilder对象“123”,然后调用delete(0, 3),我们最后调用toString方法输出StringBuilder对像的值发现是空字符串"",但是我们debug一看,会发现StringBuilder里面的value数组还是有"123"的内容,但是count是0,就是因为count是0,所以我们调用toString方法输出的是空串,以及后面就算调用append方法也是在count的基础上append,原来的"123"会被覆盖

5)reverse方法:反转

public AbstractStringBuilder reverse() {
        boolean hasSurrogates = false;
        int n = count - 1;
        for (int j = (n-1) >> 1; j >= 0; j--) {
            int k = n - j;
            char cj = value[j];
            char ck = value[k];
            value[j] = ck;
            value[k] = cj;
            if (Character.isSurrogate(cj) ||
                Character.isSurrogate(ck)) {
                hasSurrogates = true;
            }
        }
        if (hasSurrogates) {
            reverseAllValidSurrogatePairs();
        }
        return this;
    }

二、StringBuffer和StringBuilder的区别

1.StringBuffer的继承关系和StringBuilder完全一致,因此StringBuffer内部也是调用的AbstractStringBuilder的方法,所以很多方法的理解都和StringBuilder相差无几

2.但和StringBuilder不同的是,StringBuffer的每个方法上都用了synchronized修饰,当涉及到多线程调用时StringBuffer是线程安全的,与之相对的StringBuilder便线程不安全了,但因此StringBuilder的效率要比StringBuffer高

3.所以,当我们在多线程环境下应当使用StringBuffer以保证线程安全,而在单线程环境下使用StringBuilder以提高效率

  • 2
    点赞
  • 3
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值