关于StringBuilder和StringBuffer你了解多少?

一、介绍

1、StringBuilder和StringBuffer的相同点

StringBuilder和StringBuffer都代表可变字符序列的类,上节讲的String是不可变字符序列,StringBulder和StringBuffer都继承了AbstractStringBuilder,除了序列化相关的其他操作都是调用父类AbstractStringBuilder的方法实现的,两个类代码基本上相同。

2、StringBuilder和StringBuffer的不同点

StriingBuilder不是线程安全的,用在单线程环境,而StringBuffer是线程安全的,可以用在多线程环境,StringBuffer的方法都添加了synchronized关键字,synchronized就是java中的内置锁,多个线程对同一个StringBuffer操作时需要获取锁,StringBuffer为了提升性能,toString方法使用了缓存,我们看以下代码。

private transient char[] toStringCache;
    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

从代码中我们看出,如果toStringCache为空,则调用Arrays.copyOfRange(value, 0, count)拷贝当前StringBuffer内部数组的数据并赋值给toStringCache,如果StringBuffer对象没有改变,则下次调用就省去了拷贝数据的开销,直接使用toStringCache。如果StringBuffer对象有修改操作就会将toStringCache置空,下次调用toString还要重新拷贝数据,读取操作不会置空toStringCache。置空就是将toStringCache引用赋值nul,我们也看到了toString方法添加了synchronized关键字。

二、代码讲解

因为StringBuilder和StringBuffer的大部分操作都是调用父类AbstractStringBuilder的方法,因此我们除了构造函数以外只讲解AbstractStringBuilder的代码。

StringBuilder构造函数(StringBuffer类似)

  public StringBuilder() {
        super(16);
    }

无参构造函数,调用父类构造函数,创建大小为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);
    }

字符串或者字符序列为参数的构造函数,先调用父类的构造函数创建字符串或者字符序列的长度加上16,为什么加上16呢,因为StringBuilder和StringBuffer是可变字符序列,因此继续添加字符串的可能性很大,因此原长度加上16可以避免内部char数组的扩容,前提添加的字符串小于16.

AbstractStringBuilder

1、属性

char[] value;存储数据的数组
int count; 当前数量大小

2、构造方法

    //子类序列化需要
    AbstractStringBuilder() { 
    }
    //创建指定大小的char数组
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

 

3、常用方法

public void ensureCapacity(int minimumCapacity) 确保char数组容量至少等于minimumCapacity,如果当前char数组容量小于minimumCapacity,将进行扩容,扩容是新建一个当前char数组容量乘以2加2的新数组,并把当前char数组的数据拷贝到新建数组中去。如果minimumCapacity为负数将不进行任何操作。
   public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0) //1
            ensureCapacityInternal(minimumCapacity);
    }

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

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2; //3
        if (newCapacity - minCapacity < 0) { //4
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) //5
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

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

1、指定的minimumCapacity如果小于0则直接返回

2、指定的minimumCapacity大于当前char数组容量才进行扩容,先调用newCapacity计算新容量,然后调用Arrays.copyOf拷贝一个指定大小的新数组并赋值给当前对象的char数组,新数组中包含原来数组的所有值。Arrays.copyOf使用了System.arraycopy方法,上节已经介绍过,不再赘述。

3、value.length左移一位,等于乘以2,然后加2,计算扩容的数组大小,为什么乘以2呢?因为如果扩容后的数组过大会造成存储空间的浪费,如果扩容后的数组过小会造成数组的频繁扩容影响性能,因此为了权衡选择了两倍扩容,两倍扩容也是计算机领域常用方法。为什么后面还要加2呢?在value的大小为0的情况下,如果不加2newCapacity的大小是0,会造成扩容失败,当然有人会有疑问了,什么情况下当前的char数组大小为0呢,子类的构造函数不是创建了大小为16的数组吗?关键在trimToSize方法,如果当前对象的count为0,调用此方法当前数组会缩减为大小为0的数组。

4、如果根据当前char数组的大小扩容后的newCapacity小于指定的minCapacity,则使用minCapacity当作扩容后的数组大小。

5、如果newCapacity小于0(溢出)或者newCapacity大于Integer.MAX_VALUE - 8,说明指数扩容后的大小或者指定的minCapacity都过大,则调用hugeCapacity计算容量,否则返回指数扩容后的容量。

6、指定的minCapacity大于Integer.MAX_VALUE,直接抛出OOM异常

7、minCapacity大于Integer.MAX_VALUE - 8则返回minCapacity,否则返回Integer.MAX_VALUE - 8

public AbstractStringBuilder append(String str) 添加指定的字符串到当前字符序列
    public AbstractStringBuilder append(String str) {
        if (str == null)  //1
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len); //3
        str.getChars(0, len, value, count); //4
        count += len;
        return this;
    }

    private AbstractStringBuilder appendNull() { //2
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

其他append重载方法实现大都类似,不再讲解。

1、如果指定字符串为null,则调用appendNull将‘null’四个字符串添加到char数组中,而不是什么都没有添加。

2、appendNull方法先调用ensureCapacityInternal确保当前数组容量是否能够存储count+4大小的数据,然后当前char数组添加null四个字符,更新count后返回。

3、调用ensureCapacityInternal确保容量足够添加指定字符串

4、调用str.getChars(0, len, value, count)将str的数据拷贝到value从count的开头处,getChars方法内部也是调用System.arraycopy,更新count后返回。

public AbstractStringBuilder insert(int offset, String str) 指定位置处插入字符串
    public AbstractStringBuilder insert(int offset, String str) {
        if ((offset < 0) || (offset > length()))  //1
            throw new StringIndexOutOfBoundsException(offset);
        if (str == null) //2
            str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len); //3
        System.arraycopy(value, offset, value, offset + len, count - offset); //4
        str.getChars(value, offset); //5
        count += len;
        return this;
    }

1、确保offset参数是否合法,非法则抛出索引越界异常。

2、如果指定字符串为null,则将字符串指定“null”字符串,字符序列会添加"null"字符串。

3、确保容量大小是否足够。

4、 调用System.arraycopy(value, offset, value, offset + len, count - offset)将value中索引为offset以后的字符序列拷贝到offset + len处,len为指定字符串的长度,offset到offset + len处的索引存储指定的字符串str

5、调用str.getChars(value, offset)将指定字符串str拷贝到value的索引offset处,value中的索引offset到offset + len存储了指定字符串str.,更新count后返回。

AbstractStringBuilder delete(int start, int end) 删除索引start到end的字符序列
    public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)  //1
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            end = count;
        if (start > end)
            throw new StringIndexOutOfBoundsException();
        int len = end - start;
        if (len > 0) {  //2
            System.arraycopy(value, start+len, value, start, count-end);
            count -= len;
        }
        return this;

1、判断start和end是否合法

2、将val中start+len索引后的字符序列拷贝到start索引处,start索引到start+len索引的字符序列被start+len索引后的字符序列覆盖,相当与删除,更新count后返回。注意,System.arraycopy只是拷贝数组中的数据,原来位置的数据仍然没有变,只不过toString的时候只获取char数组中count范围内的字符序列。

public AbstractStringBuilder reverse() 字符序列翻转
 public AbstractStringBuilder reverse() {
        boolean hasSurrogates = false;
        int n = count - 1;
        for (int j = (n-1) >> 1; j >= 0; j--) { //1
            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) { //2
            reverseAllValidSurrogatePairs();
        }
        return this;
    }

    /** Outlined helper method for reverse() */
    private void reverseAllValidSurrogatePairs() {
        for (int i = 0; i < count - 1; i++) { //3
            char c2 = value[i];
            if (Character.isLowSurrogate(c2)) {
                char c1 = value[i + 1];
                if (Character.isHighSurrogate(c1)) {
                    value[i++] = c1;
                    value[i] = c2;
                }
            }
        }
    }

1、遍历char数组,计算j和k,j相当于中位数往左第一个字符,k相当于中位数往右一个字符,依次递减j,递增k,交换j处和k处的字符,完成char数组中的字符的翻转,hasSurrogates 记录字符数组中是否存在增补字符的标识,增补字符是由两个char组成,第一个char是高代理项,第二个char是低代理项,大家自行查询资料。

2、hasSurrogates为true,char数组中存在增补字符,调用reverseAllValidSurrogatePairs处理增补字符

3、遍历char数组,如果当前索引下存在增补字符,说明这个char是翻转后的增补字符,我们举个例子,例如1234字符串翻转后为4321,如果12是增补字符,翻转不应该改变增补字符的顺序,则翻转后应该是4312,因此我们要改变翻转后增补字符两个char的顺序,翻转前增补字符是高代理项在前,低代理项在后,翻转后低代理项在前,高代理项在后,因此代码中isLowSurrogate判断索引下的char是否是低代理项,是的话与下一个高代理项char交换顺序。

其他方法大都类似或者调用的String的方法实现,不再讲解,下篇文章给大家讲解Integer类。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章