String字符串拼接比较
@ kingkiller | Thursday, Jan 28, 2021 | 2 minutes read | Update at Thursday, Jan 28, 2021

比较 + , StringBuilder, StringBuffer三者在字符串拼接时的速度,从底层了解速度快慢的原理

StringBuilder做字符串拼接高效的原因

1.与String相比

通过分析源码,发现两者底层都是用一个数组来存储字符

public final class String 
	implements java.io.Serializable,Comparable<String>,CharSequence{
    /** The value is used for character storage */
    private final char value[];

==这里需要注意,StringBuilder本身并没有定义value数组,我们需进入其父类AbstractStringBuilder中,可以发现用来存储字符的数组value.==

/**
* The value is used for character storage
*/
char value[];

可以发现,String底层的数组是用final修饰的,是一个数组常量,而StringBuilder底层用来接收存储字符的数组是一个变量.所以前者在创建之后是没法更改,而后者可以

这里我们看一段代码

package reason;
public class Test {
	public static void main(String[] args){
        String str = "不变";
        System.out.println(str);
        str = "变了"
        System.out.println(str);
    }
}

在这里插入图片描述 结果却是str从最初的"不变",到后来的"变了",字符串str发生了改变,但String底层的数组不是常量吗?为什么值会发生改变?

其实在内存中String发生了这样的变化: 在这里插入图片描述 在内存中,我们先创建了一个str对象,并且赋值"不变",之后其实并不是在原有创建的str对象上作更改,而是又创建了一个新的字符串对象,并且赋值变了,把之前的引用类型变量str指向新创建的对象,而之前创建的对象处于等待被回收的状态,如果没有被调用,就会被JVM提供的垃圾回收机制给回收掉.

到这里我们可以发现,之所以String本身做字符串拼接执行速度慢,是因为其本质上是一个不断创建新对象,并且回收旧对象的过程.那说到StringBuilder和StringBuffer,它们创建的对象是变量,对变量操作就是对对象操作,中间不存在对象的创建和回收,所以速度比String快

那真的是这样吗?我们进入到StringBuilder封装后的源代码,查看其append方法

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

发现StringBuilder在使用append方法时,如果传进来的参数是String类型 的,会去调用其父类的append方法,也就是AbstractStringBuilder的append方法,我们进入AbstractStringBuilder

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

发现当传入的字符串是null时,会去调用appendNull方法,找到appendNull方法的源码

 private AbstractStringBuilder appendNull() {
        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;
    }

==这里需要注意,count是用来记录存储字符的数组的长度==首先会用一个变量c存放当前数组的长度,再去调用ensureCapacityInternal方法来进行数组扩容.(等会append方法也会调用这个方法)我们先找到ensureCapacityInternal的源码,它和append,appendNull一样在AbstractStringBuilder类里

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

发现了这个方法其实就是再做数组的扩容,利用的是Arrays类里的copyOf进行数组的扩容

回到上面appendNull方法中,会将当前数组的长度加4作为参数传入ensureCapacityInternal方法,将value数组扩容四个大小,并且把最后四个值改为null,所以我们利用append方法拼接一个值为null的字符串,得到的也是null

public class Test1 {
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        StringBuilder buff = new StringBuilder();
        System.out.println(buff);
        String string = null;
        buff.append(string);
        System.out.println(buff);
    }
}

在这里插入图片描述

测试发现结果的确为null

在回到append方法中,如果传的字符串不为空时,会将字符串的长度和当前value数组的长度加在一起作为参数调用ensureCapacityInternal方法,获得扩容后的数组,再去调用String类的getChars方法.进入String源码,查看getChars方法

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

能够看到这个方法是利用System.arraycopy方法将value数组的值加上新传进来的String字符串的拼接到新数组dst当中

最后append方法将扩容后的本实例传回去,做到了字符串拼接的效果.可以看到整个过程中真的没有创建新的对象,一切都是在对value这个数组变量进行操控.

但是仔细考虑还是会发现一些问题,因为数组一旦创建长度没法改变,无论是利用System.arraycopy方法还是Arrays.CopyOf方法,实际上都是再new一个新的数组来存放数据.在StringBuilder扩容的过程当中,虽说没有new一个新对象,但是当拼接的字符串不为null时,会new两个新的数组.而利用"+“这种形式虽说会创建一个新的字符串对象,但是每次创对象时只要再new一个新数组就行了.所以说实际上StringBuilder也会出现不断创建新的,并且回收旧的过程,只不过从对象变成了数组.那到底为什么StringBuilder会比String快那么多呢?

这时候决定查看以下反编译的结果,发现String用”+“这种方式做字符串拼接时,竟然调用了StringBuffer的append方法.what?

在这里插入图片描述

翻阅Thinking in java发现,原来String在做大量拼接时,会默认调用StringBuffer的append方法.这样一切就说通过了,String比StringBuilder慢的原因是因为在大量拼接时,它会不断的new一个新的StringBuffer类,然后调用append方法,会再new两个新的数组,这样就出现了大量的浪费资源情况,效率也比StringBuilder慢很多

2.与StringBuffer相比

原因很简单,查阅源码就能发现,StringBuilder里的append方法没有synchronized关键字,所以它为了追求速度放弃了线程的安全性,如下

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
  @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

super.append(str); return this; }

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

但是在单一线程的情况下,StringBuilder不会出现线程安全的问题,所以建议在单线程时使用StringBuilder会更快

java
Save as image