java四种字符串拼接方式性能分析

前几天写一个防重复提交的组件的时候,有一个操作是需要将各个字段的字符串拼接成一个requestKey。看了别人的代码,我发现,中间件这种东西,每天都要处理几百几千万的请求,但是里面很多字符串拼接的时候还是很原始的“+”号拼接,如果将所有的字符串拼接操作都换成更高效的实现方式,服务器的性能会不会要好一点,于是我简单的做了个实验对比一下我们常见的字符串拼接方法的效率。常见的四种字符串拼接方法分别是
1.直接用“+”号
2.使用String的方法concat
3.使用StringBuilder的append
4.使用StringBuffer的append

字符串拼接我分为了两种拼接方式,一种是同一个字符串大量循环拼接,第二种是大量不同的字符串每次都拼接固定数量的字符串。第二种更接近与服务器上的使用场景,因为工程上是很少有一个字符串不断拼接的,基本都是大量字符串按同一种模式拼接(比如构造上报参数)。代码如下:

public class StringConcat {
    public static void main(String[] args) {
        plus();
        concat();
        stringBuffer();
        stringBuilder();
    }

    public static void plus(){
        String initial = "";
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            initial = initial + "a";
        }
        long end = System.currentTimeMillis();
        System.out.println("plus:" + (end - start));

        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            String a = "a";
            a = a + String.valueOf(i);
            //a = a + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i);
        }
        end = System.currentTimeMillis();
        System.out.println("double plus:" + (end - start));
    }

    public static void stringBuilder(){
        StringBuilder initial = new StringBuilder("");
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            initial = initial.append("b");
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuilder:" + (end - start));

        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            StringBuilder b = new StringBuilder("b");
            b.append(String.valueOf(i))
            //b.append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i));
        }
        end = System.currentTimeMillis();
        System.out.println("double StringBuilder:" + (end - start));
    }

    public static void stringBuffer(){
        StringBuffer initial = new StringBuffer("");
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            initial = initial.append("c");
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuffer:" + (end - start));

        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            StringBuffer c = new StringBuffer("c");
            c.append(String.valueOf(i));
            //c.append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i));
        }
        end = System.currentTimeMillis();
        System.out.println("double StringBuffer:" + (end - start));
    }

    public static void concat(){
        String initial = "";
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            initial = initial.concat("d");
        }
        long end = System.currentTimeMillis();
        System.out.println("concat:" + (end - start));

        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            String d = "d";
            d = d.concat(String.valueOf(i));
            d = //d.concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i));
        }
        end = System.currentTimeMillis();
        System.out.println("double concat:" + (end - start));
    }
}

结果如下:
这里写图片描述

结果:从结果可以看出,在拼接10万次的情况下,循环拼接同一个字符串的时候,StringBuilder和StringBuffer的优势是巨大的,仅仅花了不到10ms的时间,而StringBuilder稍快一点。而直接用“+”号和concat都花费了秒级的时间。但是在对10万字符串都拼接一个串的时候,concat的情况发生了逆转,速度甚至比StringBuilder和StringBuffer更快。

原理分析

1.加号拼接
打开编译后的字节码我们可以发现加号拼接字符串jvm底层其实是调用StringBuilder来实现的,也就是说”a” + “b”等效于下面的代码片。

String a = "a";
StringBuilder sb = new StringBuilder();
sb.append(a).append("b");
String str = sb.toString();

但并不是说直接用“+”号拼接就可以达到StringBuilder的效率了,因为用“+”号每拼接一次都会新建一个StringBuilder对象,并且最后toString()方法还会生成一个String对象。在循环拼接十万次的时候,就会生成十万个StringBuilder对象,十万个String对象,这简直就是噩梦。

2.concat拼接
concat的源代码如下,可以看到,concat其实就是申请一个char类型的buf数组,将需要拼接的字符串都放在这个数组里,最后再转换成String对象。

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

3.StringBuilder/StringBuffer
这两个类实现append的方法都是调用父类AbstractStringBuilder的append方法,只不过StringBuffer是的append方法加了sychronized关键字,因此是线程安全的。append代码如下,他主要也是利用char数组保存字符,通过ensureCapacityInternal方法来保证数组容量可用还有扩容。

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

他扩容的方法的代码如下,可见,当容量不够的时候,数组容量右移1位(也就是翻倍)再加2,以前的jdk貌似是直接写成int newCapacity = (value.length * 2) + 2,后来优化成了右移,可见,右移的效率还是比直接乘更高的。

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

原因分析

1.循环拼接字符串
通过实验我们发现,在循环拼接同一个字符串的时候,他们效率的按快慢排序是
StringBulider > StringBuffer >> String.concat > “+”。
StringBulider比StringBuffer更快这个容易理解,因为StringBuffer的方法是sychronized修饰的,同步的时候会损耗掉一些性能。StringBulider和String.concat的区别,主要在扩容上,String.concat是需要多少扩多少,而StringBulider是每次翻两倍,指数级扩容。在10万次拼接中,String.concat需要扩容10万次,而StringBuilder只需要扩容log100000次(大约17次),除此之外,concat每次都会生成一个新的String对象,而StringBuilder则不必,那StringBuilder如此快就不难解释了。至于直接用“+”号连接,之前已经说了,它会产生大量StringBuilder和String对象,当然就最慢了。
2.大量字符串拼接
在只拼接少量字符串的情况下的时候,他们效率的按快慢排序是
String.concat > StringBulider > StringBuffer > “+”。
为什么在拼接少量字符串的时候,String.concat就比StringBuilder快了呢,原因大致有两点,一是StringBuilder的调用栈更深,二是StringBuilder产生的垃圾对象更多,并且它重写的toString方法为了做到不可变性采用了“保护性拷贝”,因此效率不如concat。
详细原因分析参考:concat和StringBuilder性能分析
保护性拷贝见:保护性拷贝
当拼接的字符串少的时候,concat因为上述优势略快,但是当一次性拼接字符串多的时候,StringBuilder的扩容更少优势便会开始展现出来,例如一次拼接8个字符串,concat的效率就已经明显不如StringBuilder了,如下图。
一次拼接8个字符串

结论

从以上分析我们可以得出以下几点结论
1.无论如何直接用“+”号连接字符串都是最慢的
2.在拼接少数字符串(不超过4个)的时候,concat效率是最高的
3.多个字符串拼接的时候,StringBuilder/StringBuffer的效率是碾压的
4.在不需要考虑线程安全问题的时候,使用StringBuilder的效率比StringBuffer更高

  • 24
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Java 字符串拼接有多种方式可以选择,不同的方式适用于不同的场景和要求。 1. 使用"+"运算符:这是最常见的字符串拼接方式。它简单直接,适用于少量字符串拼接操作。例如: ```java String str = "Hello" + " " + "World"; ``` 然而,当需要进行大量字符串拼接操作时,使用"+"运算符会频繁创建新的字符串对象,导致性能下降。 2. 使用StringBuilder类:StringBuilder是Java提供的可变字符串类,它可以高效地进行字符串拼接。例如: ```java StringBuilder sb = new StringBuilder(); sb.append("Hello"); sb.append(" "); sb.append("World"); String str = sb.toString(); ``` 使用StringBuilder可以避免频繁创建新的字符串对象,从而提升性能。它适用于需要进行大量字符串拼接操作的场景。 3. 使用StringBuffer类:StringBuffer类与StringBuilder类相似,也是可变字符串类,可以高效地进行字符串拼接。然而,与StringBuilder不同的是,StringBuffer是线程安全的。例如: ```java StringBuffer sb = new StringBuffer(); sb.append("Hello"); sb.append(" "); sb.append("World"); String str = sb.toString(); ``` 如果在多线程环境下进行字符串拼接操作,可以选择使用StringBuffer。 综上所述,对于少量字符串拼接操作,可以使用"+"运算符;对于大量字符串拼接操作,建议使用StringBuilder;而在多线程环境下进行字符串拼接操作时,可以选择使用StringBuffer。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值