当前位置 博文首页 > 外星喵的博客:面试常问关于Stirng、StringBuffer和StringBuilde

    外星喵的博客:面试常问关于Stirng、StringBuffer和StringBuilde

    作者:[db:作者] 时间:2021-07-12 15:41

    介绍

    基本对比:

    • String:String对象是不可变的。对String对象的任何改变都不影响到原对象的内容,相关的任何change操作都会导致该字符串变量指向新的对象的地址。
    • StringBuilder:StringBuilder是可变的(存入的值的内容和长度都是可改变的,且引用地址不变),它不是线程安全的。
    • StringBuffer:StringBuffer也是可变的,它是线程安全的,所以它的开销比StringBuilder大
    Q:这三个类都是final类型,为何又说String对象是不可变的,StringBuilder和StringBuffer是可变的?
    A: final修饰类代表此类不可被继承,并非其包含内容不可改变,在这三个类中都分别维护了一个char类型数组,其中只有String中此数组被final所修饰(final修饰的变量被初始化后其值不可改变),对于StringBuilder和StringBuffer所维护的char[]是可变的,每次append操作都是在此数组的基础上进行arraycopy操作。

    性能测试

    测试源码:

        public static void performanceTest(int frequency) {
            String str = "";
            StringBuilder stringBuilder = new StringBuilder();
            StringBuffer stringBuffer = new StringBuffer();
    
            long totalTime = 0;//记录每个对象记录测试总耗时
            long time;         //记录每个对象记录一个周期测试耗时
            int i = 0;
            int cycle = 10;    //测试周期 
            for (int j = 0; j < cycle; j++) {
                //时间单位为纳秒
                time = System.nanoTime();
                while (i++ < frequency) {
                    str += "A";
                }
                totalTime += System.nanoTime() - time;
                str = "";
            }
            str = null;
            System.gc();//进行一次垃圾回收,避免缓存对后续测试的影响。
            System.out.println("str          累加" + frequency + "个长度为1的字符串,平均耗时为:" + totalTime / cycle);
    
            totalTime = i = 0;
            for (int j = 0; j < cycle; j++) {
                time = System.nanoTime();
                while (i++ < frequency) {
                    stringBuffer.append("A");
                }
                totalTime += System.nanoTime() - time;
                stringBuffer = new StringBuffer();
            }
            stringBuffer = null;
            System.gc();
            System.out.println("stringBuffer 累加" + frequency + "个长度为1的字符串,平均耗时为:" + totalTime / cycle);
    
            totalTime = i = 0;
            for (int j = 0; j < cycle; j++) {
                time = System.nanoTime();
                while (i++ < frequency) {
                    stringBuilder.append("A");
                }
                totalTime += System.nanoTime() - time;
                stringBuilder = new StringBuilder();
            }
            stringBuilder = null;
            System.gc();
            System.out.println("stringBuilder累加" + frequency + "个长度为1的字符串,平均耗时为:" + totalTime / cycle);
    
            System.out.println();
        }
    
        public static void main(String[] args) {
            performanceTest(100);
            performanceTest(10000);
            performanceTest(1000000);
        }
    

    测试结果(时间单位为纳秒,100000纳秒 = 1毫秒):

    在这里插入图片描述
    在大量字符串拼接的场景中,如果对象被定义成String类型,会产生很多无用的中间对象,浪费内存空间,效率低。

    这时,我们可以用更高效的可变字符序列:StringBuilder和StringBuffer,来定义对象。

    那么,StringBuilder和StringBuffer有啥区别?

    StringBuffer对各主要方法加了synchronized关键字,而StringBuilder没有。所以,StringBuffer是线程安全的,而StringBuilder不是。

    其实,我们很少会出现需要在多线程下拼接字符串的场景,所以StringBuffer实际上用得非常少。一般情况下,拼接字符串时我们推荐使用StringBuilder,通过它的append方法追加字符串,它只会产生一个对象,而且没有加锁,效率较高。

    String a = “123”;
    String b = “456”;
    StringBuilder c = new StringBuilder();
    c.append(a).append(b);
    System.out.println?;
    接下来,关键问题来了:字符串拼接时使用String类型的对象,效率一定比StringBuilder类型的对象低?

    答案是否定的。

    为什么?

    使用javap -c StringTest命令反编译:

    图片

    从图中能看出定义了两个String类型的参数,又定义了一个StringBuilder类的参数,然后两次使用append方法追加字符串。

    如果代码是这样的:

    String a = “123”;
    String b = “789”;
    String c = a + b;
    System.out.println?;
    使用javap -c StringTest命令反编译的结果会怎样呢?

    图片

    我们会惊讶的发现,同样定义了两个String类型的参数,又定义了一个StringBuilder类的参数,然后两次使用append方法追加字符串。跟上面的结果是一样的。

    其实从jdk5开始,java就对String类型的字符串的+操作做了优化,该操作编译成字节码文件后会被优化为StringBuilder的append操作。

    结论:

    • 当拼接的字符串数量较小时,String、StringBuffer、StringBuild执行耗时可以忽略不记。

    • StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,所以StringBuffer比StringBuild多了一部分用于线程同步的开销。

    • 随着拼接的字符串数量越来越大,String性能的下降尤为明显,按性能由小到大为:String < StringBuffer < StringBuilder。

    使用建议:

    • 循环外字符串拼接可以直接使用String的+操作,没有必要通过StringBuilder进行append.
    • 有循环体的话,好的做法是在循环外声明StringBuilder对象,在循环内进行手动append。不论循环多少层都只有一个StringBuilder对象。
    • 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

    补充问题:

    String str="hello world"和String str=new String(“hello world”)的区别?

    String str=“hello world”

    通过直接赋值的形式可能创建一个或者不创建对象,如果"hello world"在字符串池中不存在,会在java字符串池中创建一个String对象(“hello world”),常量池中的值不能有重复的,所以当你通过这种方式创建对象的时候,java虚拟机会自动的在常量池中搜索有没有这个值,如果有的话就直接利用他的值,如果没有,他会自动创建一个对象,所以,str指向这个内存地址,无论以后用这种方式创建多少个值为”hello world”的字符串对象,始终只有一个内存地址被分配。

    String str=new String(“hello world”)

    通过new 关键字至少会在JVM堆中创建一个对象,也有可能创建两个(取决于字符串常量池是否存在此字符串)。

    因为用到new关键字,肯定会在堆中创建一个String对象,如果字符池中已经存在"hello world",则不会在字符串池中创建一个String对象,如果不存在,则会在字符串常量池中也创建一个对象。他是放到堆内存中的,这里面可以有重复的,所以每一次创建都会new一个新的对象,所以他们的地址不同。

    相对于StringBuffer和StringBuilder,String 有一个intern() 方法,用来检测在String常量池是否已经有这个String存在。

    cs
    下一篇:没有了