面试常问关于Stirng、StringBuffer和StringBuilder的区别

介绍

基本对比:

  • 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存在。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值