深入探索String、StringBuffer和StringBuilder区别(源码 + 代码验证)

深入探索String、StringBuffer和StringBuilder区别(源码 + 代码验证),第1张

String、StringBuffer和StringBuilder都是Java中用来表示字符串的。在Java中,String类是一个不可变类,任何对String的改变都会引发新的String对象的生成。StringBuffer和StringBuilder则都是可变类,任何对他所指代的字符串都不会产生新的对象。StringBuilder是Java5后提出来的,与StringBuffer相比,StringBuilder有更高的执行效率,但其不是线程安全的。

1. 性能对比

下面对String,StringBuffer和StringBuilder进行append *** 作的性能对比。主要比对代码如下:

public static void compareAddTime(String string,StringBuilder stringBuilder,StringBuffer stringBuffer){
        long preTime,afterTime;

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 100000; ++i){
            string = string + 'a';
        }
        afterTime = System.currentTimeMillis();
        System.out.println("String操作10000遍需要时长为:" + (afterTime - preTime) + "ms");

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 10000000; ++i){
            stringBuffer.append('a');
        }

        afterTime = System.currentTimeMillis();
        System.out.println("StringBuffer操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 10000000; ++i){
            stringBuilder.append('a');
        }
        afterTime = System.currentTimeMillis();
        System.out.println("StringBuilder操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
    }

执行以上方法,得出如下结果图:

由于String每次" + “ *** 作都要new新对象,因此仅” + "10000次,否则容易内存溢出。我们可以看见,String具有最低的性能效率,在相同数量级下StringBuilder *** 作效率要高于StringBuilder,且当其数量级越高,差距越明显。因此在实际中我们应该尽量少用String对字符串进行 *** 作,不仅效率低下,还容易导致频繁垃圾回收,影响代码效率。

1.1 String " + " *** 作具体实现

为了更直观查看String是如何new新对象的,我对String进行了以下探索:

    public static void main(String[] args){
        String string = new String();
        
        string = string + "abc";
    }

针对如上简单的" + " *** 作,使用javac指令对其进行编译,并使用javap -c指令对其进行反编译。得到如下结果

 Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: invokespecial #3                  // Method java/lang/String."":()V
       7: astore_1
       8: new           #4                  // class java/lang/StringBuilder
      11: dup
      12: invokespecial #5                  // Method java/lang/StringBuilder."":()V
      15: aload_1
      16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: ldc           #7                  // String abc
      21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: astore_1
      28: return

我们可以看到,当进行" + " *** 作时,Java会先new一个新的StringBuilder对象,并对其进行append *** 作,最后返回toString()字符串。反应到Java代码,即为如下过程:

new StringBuilder().append(string).append("abc").toString()

(Tips:当String并没有 *** 作String对象,而是 *** 作两个独立字符串时(即:string = “abc” + “def”),JVM对其进行性能优化,此时无需new StringBuilder用于拼接)

1.2 StringBuffer和StringBuilder类append *** 作对比

针对StringBuffer和StringBuilder,我们可以查看他们的实现方式。

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

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

如上所示,在StringBuffer中,使用了synchronized保证其线程安全性。此外在StringBuffer中有一个toStringCache数组,用以缓存StringBuffer对象toString后结果。当StringBuffer发生修改了字符串的 *** 作时,需要把缓存清除。

StringBuffer和StringBuilder均继承了AbstractStringBuilder类,因此他们的append方法均一致。查看其append实现方法,其实现代码如下所示:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    //判断是否有足够空间存储str,若不足,触发扩容
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

可以看到在append其,需要判断类对象是否有充足空间,是否需要扩容。其基本逻辑与ArratList和LinkedList类似(想了解更多关于ArrayList和LinkedList,可以看Java基础(一)-ArrayList和LinkedList性能对比与原因探索)。他们均有一个最大值MAX_ARRAY_SIZE(即Integer.MAX_VALUE - 8,为何要减8在ArrayList中阐述,这里不在解释),当容量不足时进行扩容:

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    //若扩容后容量大于MAX_ARRAY_SIZE或者int已溢出,则把预留的 - 8位也用于数组,此时可能导致内存溢出
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

可以看到其扩容机制为原有长度 * 2 + 2(这里 + 2应该是避免0 * 2仍是0导致的无法扩容情况),若仍不足,扩容到需要的最小长度minCapacity。若扩容后容量大于MAX_ARRAY_SIZE或者int已溢出,则把预留的 - 8位也用于数组,此时可能导致内存溢出。

2. 线程安全性对比

我们可以看到,在StringBuffer中用synchronized保证了线程安全,在StringBuilder中则没有。且String每次都会生成新对象。这里编写代码测试他们的线程安全性:

AppendThread类:

用于对StringBuffer,StringBuilder,String进行append *** 作的线程类。

package com.thread.demo.string;

public class AppendThread extends Thread{
    private StringBuffer stringBuffer;
    private StringBuilder stringBuilder;
    private String string;

    public AppendThread(StringBuffer stringBuffer,StringBuilder stringBuilder,String string){
        this.stringBuffer = stringBuffer;
        this.stringBuilder = stringBuilder;
        this.string = string;
    }

    @Override
    public void run(){
        for(int i = 0;i < 1000; ++i){
            stringBuffer.append("a");
            stringBuilder.append("a");
            string = string + "a";
        }
        System.out.println("StringBuffer Size:" + stringBuffer.length() + " | " + "StringBuilder Size:" + stringBuilder.length() + " | " + "String Size:" + string.length());
    }
}

Main方法:

new十个新线程,同步append。

package com.thread.demo.string;

public class stringCompare {

    public static void main(String[] args){
        StringBuffer stringBuffer = new StringBuffer();
        StringBuilder stringBuilder = new StringBuilder();
        String string = new String();

        //测试运行时间
        //compareAddTime(string,stringBuilder,stringBuffer);

        //string = string + "abc";

        //验证线程安全性
        for(int i = 0;i < 10; ++i){
            new AppendThread(stringBuffer,stringBuilder,string).start();
        }
    }

    /**
     * @Description 查看String,StringBuilder和StringBuffer操作时长
     * @author Sc_Cloud
     * @param  * @param string
     * @param stringBuilder
     * @param stringBuffer
     * @return void
     * @date 2022/4/8 19:44
     */
    public static void compareAddTime(String string,StringBuilder stringBuilder,StringBuffer stringBuffer){
        long preTime,afterTime;

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 100000; ++i){
            string = string + 'a';
        }
        afterTime = System.currentTimeMillis();
        System.out.println("String操作10000遍需要时长为:" + (afterTime - preTime) + "ms");

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 10000000; ++i){
            stringBuffer.append('a');
        }

        afterTime = System.currentTimeMillis();
        System.out.println("StringBuffer操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");

        preTime = System.currentTimeMillis();
        for(int i = 0;i < 10000000; ++i){
            stringBuilder.append('a');
        }
        afterTime = System.currentTimeMillis();
        System.out.println("StringBuilder操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
    }
}

正常情况下十个线程append1000次,长度应该为10000。实际执行效果如下:

可以看到StringBuffer最大值为10000,StringBuilder最大值小于10000,String由于每次需要new新对象,因此长度均为单个线程append长度1000,符合预期。

总结

因此,对String、StringBuffer和StringBuilder,我们一般有如下结论:

  • String在对字符串进行 *** 作时会生成新的对象;
  • StringBuffer和StringBuilder会 *** 作原有对象,减少新生成对象;
  • StringBuffer是线程安全的,效率低;
  • StringBuilder不是线程安全的,但是执行效率高;

由于StringBuilder相较于StringBuffer有速度优势,因此多数情况下建议使用StringBuilder

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/742197.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-28
下一篇 2022-04-28

发表评论

登录后才能评论

评论列表(0条)

保存