zoukankan      html  css  js  c++  java
  • 无聊的笔记:之一(Java字符串链接方式效率对比和解析)

    简介

    JAVA开发过程中,经常会遇到字符串操作,对字符串的拼接操作更常见。
    拼接字符串主要有以下几种方法:

    1. "" + ""
    2. "".concat("")
    3. new StringBuilder().append()
    4. new StringBuffer().append()
    

    还有一个StringUtils工具类的join()方法,这里不做讨论。
    时间紧迫的朋友可以跳过分析,直接看结论。

    分析

    1. 直接使用+拼接字符串

    这是最方便的。但是它的性能在大部分情况下也是最低的一个。为什么这么说呢?请看官继续向下:

    废话少说,先上个例子:

    public class Test1 {
        public static void main(String[] args) {
            String s = "abc";
            String s1 = "123" + s;
            String s2 = "efg" + s1;
        }
    }
    

    我们使用javap工具对字节码文件 Test1.class 反编译一下,瞅瞅+操作符干了些啥。

    javap是java字节码反编译工具。-c 参数表示显示反汇编命令。

    C:comdemo> javap -c .Test1.class
    Compiled from "Test1.java"
    public class com.demo.Test1 {
      public com.demo.Test1();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: ldc           #2                  // String abc
           2: astore_1
           3: new           #3                  // class java/lang/StringBuilder
           6: dup
           7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
          10: ldc           #5                  // String 123
          12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          15: aload_1
          16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          22: astore_2
          23: new           #3                  // class java/lang/StringBuilder
          26: dup
          27: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
          30: ldc           #8                  // String efg
          32: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          35: aload_2
          36: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          42: astore_3
          43: return
    }
    

    从反编译的代码中可以看出,编译器会把+符号优化成StringBuilder类,并使用 StringBuilder.append() 方法对字符拼接。最后调用 StringBuilder.toString() 方法,返回String。
    在上面的代码中,我们对字符串进行了两次+操作,在优化后的代码也创建两次StringBuilder对象和调用两次toString()方法。

    既然编译器会把+操作优化成StringBuilder方法,那它们效率会一样?其实不然。

    2. 为什么+操作符比StringBuilder效率低?

    首先看看StringBuilder怎么拼接字符串。

    StringBuffer sb = new StringBuffer();
    sb.append("abc");
    sb.append("123");
    sb.append("efg");
    sb.toString();
    

    上面的代码,只 new 了一个 StringBuilder 对象,而且只调用了一次 toString()方法。
    我们知道在java中实例化对象,其实是很费时的,还要回收什么的。要不就不会搞出 单例模式 这种东西了。
    toString()这个方法更是浪费时间,我们来看下 StringBuildertoString() 方法的源码。

    public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
        
        //省略 n 多代码
        private transient char[] toStringCache;     
    
        @Override
        public synchronized String toString() {
            if (toStringCache == null) {  
                toStringCache = Arrays.copyOfRange(value, 0, count);
            }
            return new String(toStringCache, true);
        }
    
        //省略 n 多代码    
    
    }
    

    StringBuilderStringBuffer 都是 AbstractStringBuilder 的实现类,其底层是用 char[] 数组来储存数据的。StringBuilder默认初始char[]大小是16,当添加的字符大于16个,就会自动扩容。扩容这个操作本质是对数组的复制,也挺费时的,有兴趣的可以看一下源码。而+操作生成的StringBuilder默认大小为16,如果拼接的字符串过大,会频繁的扩容导致效率低下。

    所以如果使用StringBuilder的时候,我们可以给一个参数capacity,初始化char[]的大小,这样可以避免频繁的扩容。

    toString() 方法就是对 char[] 数组的复制。这可是个挺费时间的操作。

    所以,虽然编译器对 + 操作进行了优化,但是由于频繁的实例化StringBuilder、频繁的调用toString()、在添加的字符串较大的情况下还会频繁的扩容,导致其效率极其低下。

    但是,+一定比StringBuilder效率低嘛?答案当然是否定的。

    3. +什么时候比StringBuilder高效?

    我们再上两段代码:

    public static void main(String[] args) {
        // +
        String s1 = "abc"+"123"+"efg";
    
        // StringBuilder
        StringBuilder sb = new StringBuilder();
        String s2 = sb.append("abc").append("123").append("efg").toString();
    }
    

    同样把这段代码反编译一下,你知道,编译器把上面的代码编译成什么样了嘛?

    public static void main(String[] args) {
        String s1 = "abc123efg";
        StringBuilder sb = new StringBuilder();
        String s2 = sb.append("abc").append("123").append("efg").toString();
    }
    

    编译器直接在编译的时候就把 + 给组合完成了,运行时间为 0 。谁效率高谁效率低显而易见。

    所以进行大量的字符串拼接操作 StringBuilder 更合适,而进行少量的,可预知的字符串拼接 + 更合适。(一般 for 循环中用 StringBuilder ,而静态变量等用 +

    4. StringBuilderStringBuffer的区别。

    它们两个的区别主要是 StringBuilder 是线程不安全的,而StringBuffer是线程不安全的。
    我们看一下两个类的 append() 方法的源码:
    StringBuilder

    public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
        // ...
        @Override
        public StringBuilder append(String str) {
            super.append(str);
            return this;
        }
        // ...
    }
    

    StringBuffer

    public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
        // ...
        @Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
        // ...
    }
    

    StringBuilder比StringBuffer少了同步锁。其他基本相同,因为它们都是AbstractStringBuilder的实现类。是双胞胎(其实眼瞎的我莫名感觉他们的名字长得还蛮像的)

    所以,在线程安全的情况下,StringBuilder更高效一点(毕竟同步锁也要花时间),而线程不安全就只能用 StringBuffer。

    5. String.concat()拼接与StringBuilder的比较

    上代码:

    
    String s = "abc".concat("123").concat("efg");
    
    

    来瞅瞅源码:

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

    String.concat()方法调用了一次Arrays.copyOf(value, len + otherLen)方法,一次性分配了两个个字符串长度的内存空间,只执行了一次空间分配,并对拼接的两个字符各复制了一次,复制了两次。
    StringBuilderappend()char[]未满的情况下,不会扩容。只会在初始化的时候执行一次空间分配,对两个字符串各复制了一次,复制了两次,但是最后会调用toString(),还会复制一次。

    所以,比较之下,在只有两个字符串拼接的情况下,concat的效率要高一点,而大量的字符串拼接,StringBuilder效率会高一点,而且好在初始化的时候,指定char[]容器的大小,这样可以避免过度的扩容

    测试环境

    • 系统:windows10 x64
    • 处理器:i7 9700k
    • 内存:16G
    • 台式电脑

    测试代码

    public class Demo {
    
        public static void main(String[] args) {
            int number = 10000000; //拼接次数
            long start ;           //开始时间
    
            // concat
            String s2 = "";
            start = System.currentTimeMillis();
            for(int i = 0 ; i < number ; ++i){
                s2.concat(String.valueOf(i));
            }
            System.out.println(" concat 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
    
    
            // StringBuilder
            StringBuilder stringBuilder = new StringBuilder();
            start = System.currentTimeMillis();
            for(int i = 0 ; i < number ; ++i){
                stringBuilder.append(String.valueOf(i));
            }
            System.out.println("StringBuilder 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
    
            //StringBuffer
            StringBuffer stringBuffer = new StringBuffer();
            start = System.currentTimeMillis();
            for(int i = 0 ; i < number ; ++i){
                stringBuffer.append(String.valueOf(i));
            }
            System.out.println(" StringBuffer 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
    
            // +
            String s = "";
            start = System.currentTimeMillis();
            for(int i = 0 ; i < number ; ++i){
                s += String.valueOf(i);
            }
            System.out.println(" + 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");
    
    
        }
    }
    

    测试结果

    测试结果取几次或者1次的平均值整数,不同运行环境会有偏差

    + concat StringBuilder StringBuffer
    1000次 3ms 1ms 1ms 1ms
    100000次 15584ms 10ms 6ms 6ms
    1000000次 2212323ms 56ms 34ms 44ms
    10000000次 很多很多ms 408ms 353ms 448ms

    结论

    1. +最好不要直接大量使用。只有在逻辑较简单的情况下(没有for循环之类)或者字符确定的情况下时候使用。如:String a = "abc"+"123"
    2. 一般情况,在大量字符串拼接操作中。使用StringBuilder和StringBuffer,并尽可能的估算字符串的大小,使用带capacity参数的构造函数,也就是char[]的大小,这样可以避免重复的扩容。
    3. StringBuffer 在线程不安全的情况下使用,其他情况一般使用StringBuilder
    4. 只有两个字符串拼接的时候使用 concat(),这个性能最好。 但是每次拼接操作都会分配内存的操作,而上面两个并不一定每次拼接操作都会分配内存。

    作者:BobC

    文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。
  • 相关阅读:
    Oracle普通表->分区表转换(9亿数据量)
    RHEL6.4 + Oracle 11g DG测试环境快速搭建参考
    java 获取时间戳的三种方式
    java sm3加密算法
    java byte数组与String互转
    Java的多线程
    最大重叠点
    23. 客户默认选项(Default Customer Options)
    Android Studio 1.3RC版 build加速
    查看linux机器是32位还是64位的方法
  • 原文地址:https://www.cnblogs.com/Eastry/p/13605064.html
Copyright © 2011-2022 走看看