深入分析Java使用+和StringBuilder进行字符串拼接的差异

时间:2022-03-09 21:54:06

转自:http://bsr1983.iteye.com/blog/1935856


String a="a";
String b="b";
String d="d";
String c = a+b+d;
!!!   只会new 一个stringBuilder对象!并进行2次append操作,能后在调用stringBuilder的tostring方法,返回给string对象
!!!   如果是for 循环中的+, 每循环一次new 一个Stringbuilder对象!

连+,最后一个分号只创建一个StringBuilder
一旦连接操作由多条语句执行,就是一条语句一个StringBuilder

只有在循环时使用+来拼接会产生此类问题,如果在非循环下,+和StringBuilder之类的基本没有任何分别。

如果在编写代码的过程中大量使用+进行字符串评价还是会对性能造成比较大的影响,但是使用的个数在1000以下还是可以接受的,大于10000的话,执行时间将可能超过1s,会对性能产生较大影响。

深入分析Java使用+和StringBuilder进行字符串拼接的差异

 

       今天看到有网友在我的博客留言,讨论java中String在进行拼接时使用+和StringBuilder和StringBuffer中的执行速度差异很大,而且之前看的书上说java在编译的时候会自动将+替换为StringBuilder或StringBuffer,但对于这些我都没有做深入的研究,今天准备花一点时间,仔细研究一下。

       首先看一下java编译器在编译的时候自动替换+为StringBuilder或StringBuffer的部分,代码如下。

       测试环境为win764位系统,8G内存,CPU为 i5-3470,JDK版本为32位的JDK1.6.0_38

       第一次使用的测试代码为:

         

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. public static void main(String[] args) {  
  2.      // TODO Auto-generated method stub  
  3.      String demoString="";  
  4.      int execTimes=10000;  
  5.      if(args!=null&&args.length>0)  
  6.      {  
  7.          execTimes=Integer.parseInt(args[0]);  
  8.      }  
  9.      System.out.println("execTimes="+execTimes);  
  10.      long starMs=System.currentTimeMillis();  
  11.      for(int i=0;i<execTimes;i++)  
  12.      {  
  13.          demoString=demoString+i;  
  14.      }  
  15.      long endMs=System.currentTimeMillis();  
  16.      System.out.println("+ exec millis="+(endMs-starMs));  
  17.   }  

 

   输入不同参数时的执行时间如下:

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. C:\>java StringAppendDemo 100  
  2. execTimes=100  
  3. + exec millis=0  
  4. C:\>java StringAppendDemo 1000  
  5. execTimes=1000  
  6. + exec millis=6  
  7. C:\>java StringAppendDemo 10000  
  8. execTimes=10000  
  9. + exec millis=220  
  10. C:\>java StringAppendDemo 100000  
  11. execTimes=100000  
  12. + exec millis=44267  

 

可以看到,输入的参数为10000和100000时,其执行时间从0.2秒到了44秒。

我们先使用javap命令看一下编译后的代码:

javap –c StringAppendDemo

这里我摘录了和循环拼接字符串有关的那部分代码,具体为:

  

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. 51:  lstore_3  
  2.   52:  iconst_0  
  3.   53:  istore  5  
  4.   55:  iload   5  
  5.   57:  iload_2  
  6.   58:  if_icmpge       87  
  7.   61:  new     #5; //class java/lang/StringBuilder  
  8.   64:  dup  
  9.   65:  invokespecial   #6; //Method java/lang/StringBuilder."<init>":()V  
  10.   68:  aload_1  
  11.   69:  invokevirtual   #8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  
  12.   72:  iload   5  
  13.   74:  invokevirtual   #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;  
  14.   77:  invokevirtual   #10; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;  
  15.   80:  astore_1  
  16.   81:  iinc    5, 1  
  17.   84:  goto    55  

 

可以看到,之前的+的确已经被编译为了StringBuilder对象的append方法。通过这里的字节码可以看到,对于每一个+都将被替换为一个StringBuilder而不是我所想象的只生成一个对象。也就是说,如果有10000个+号就会生成10000个StringBuilder对象。具体参看上面字节码的第88行,此处是执行完一次循环以后,再次跳转到55行去执行。

接着,我们把再写一个使用StringBuilder直接实现的方式,看看有什么不一样。

具体代码为:

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. public class StringBuilderAppendDemo {  
  2.        public static void main(String[] args) {  
  3.        // TODO Auto-generated method stub  
  4.        String demoString="";  
  5.        int execTimes=10000;  
  6.        if(args!=null&&args.length>0)  
  7.        {  
  8.            execTimes=Integer.parseInt(args[0]);  
  9.        }  
  10.        System.out.println("execTimes="+execTimes);  
  11.        long starMs=System.currentTimeMillis();  
  12.        StringBuilder strBuilder=new StringBuilder();  
  13.        for(int i=0;i<execTimes;i++)  
  14.        {  
  15.            strBuilder.append(i);  
  16.        }  
  17.        long endMs=System.currentTimeMillis();  
  18.        System.out.println("StringBuilder exec millis="+(endMs-starMs));  
  19.     }  
  20. }  

 

和上次一样的参数,看看执行时间的差异

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. C:\>java StringBuilderAppendDemo 100  
  2. execTimes=100  
  3. StringBuilder exec millis=0  
  4. C:\>java StringBuilderAppendDemo 1000  
  5. execTimes=1000  
  6. StringBuilder exec millis=1  
  7. C:\>java StringBuilderAppendDemo 10000  
  8. execTimes=10000  
  9. StringBuilder exec millis=1  
  10. C:\>java StringBuilderAppendDemo 100000  
  11. execTimes=100000  
  12. StringBuilder exec millis=5  

 

可以看到,这里的执行次数上升以后,执行时间并没有出现大幅度的增加,那我们在看一下编译后的字节码。

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. 51:  lstore_3  
  2.  52:  new     #5; //class java/lang/StringBuilder  
  3.  55:  dup  
  4.  56:  invokespecial   #6; //Method java/lang/StringBuilder."<init>":()V  
  5.  59:  astore  5  
  6.  61:  iconst_0  
  7.  62:  istore  6  
  8.  64:  iload   6  
  9.  66:  iload_2  
  10.  67:  if_icmpge       84  
  11.  70:  aload   5  
  12.  72:  iload   6  
  13.  74:  invokevirtual   #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;  
  14.  77:  pop  
  15.  78:  iinc    6, 1  
  16.  81:  goto    64  

 

通过字节码可以看到,整个循环拼接过程中,只在56行对StringBuilde对象进行了一次初始化,以后的拼接操作的循环都是从64行开始,然后到81行进行goto 64再次循环。

为了证明我们的推断,我们需要看看虚拟机中是否是这么实现的。

参考代码:http://www.docjar.com/html/api/com/sun/tools/javac/jvm/Gen.java.html

具体的方法,标红的地方就是在语法树处理过程中的一个用来处理字符串拼接“+”号的例子,其他部分进行的处理也类似,我们只保留需要的部分

 

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. public void visitAssignop(JCAssignOp tree) {  
  2.  OperatorSymbol operator = (OperatorSymbol) tree.operator;  
  3.  Item l;  
  4.  if (operator.opcode == string_add) {  
  5.  // Generate code to make a string buffer  
  6.  makeStringBuffer(tree.pos());  
  7.    
  8.  // Generate code for first string, possibly save one  
  9.  // copy under buffer  
  10.  l = genExpr(tree.lhs, tree.lhs.type);  
  11.  if (l.width() > 0) {  
  12.  code.emitop0(dup_x1 + 3 * (l.width() - 1));  
  13.  }  
  14.    
  15.  // Load first string and append to buffer.  
  16.  l.load();  
  17.  appendString(tree.lhs);  
  18.    
  19.  // Append all other strings to buffer.  
  20.  appendStrings(tree.rhs);  
  21.    
  22.  // Convert buffer to string.  
  23.  bufferToString(tree.pos());  
  24.  }  
 剩余代码已删除。

 

而具体把+转换为StringBuilder的方法为:

 

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. void makeStringBuffer(DiagnosticPosition pos) {  
  2.  code.emitop2(new_, makeRef(pos, stringBufferType));  
  3.  code.emitop0(dup);  
  4.  callMethod(  
  5.  pos, stringBufferType, names.init, List.<Type>nil(), false);  
  6.  }  
 

 

看标红出的代码可以知道,此处调用了stringBufferType的init方法来进行初始化。

看到此处有同学一定会有疑问,刚刚的字节码不是显示替换成StringBuilder了吗?原因在这里:

protected Gen(Context context)(95行)这个方法的代码,发现其中包含了stringBufferType变量的初始化:
stringBufferType = target.useStringBuilder() ? syms.stringBuilderType
                : syms.stringBufferType;(108、109、110行)
通过一个三目运算符,根据当前的编译的目标JDK是否启用了StringBuilder来设置stringBufferType的真正类型。
回到处理“+”的代码,调用完makeStringBuffer方法后接着调用appendStrings方法和bufferToString方法。具体代码如下

 

Java代码  深入分析Java使用+和StringBuilder进行字符串拼接的差异
  1. /** Add all strings in tree to string buffer. 
  2.  */  
  3.  void appendStrings(JCTree tree) {  
  4.  tree = TreeInfo.skipParens(tree);  
  5.  if (tree.getTag() == JCTree.PLUS && tree.type.constValue() == null) {  
  6.  JCBinary op = (JCBinary) tree;  
  7.  if (op.operator.kind == MTH &&  
  8.  ((OperatorSymbol) op.operator).opcode == string_add) {  
  9.  appendStrings(op.lhs);  
  10.  appendStrings(op.rhs);  
  11.  return;  
  12.  }  
  13.  }  
  14.  genExpr(tree, tree.type).load();  
  15.  appendString(tree);  
  16.  }  
  17.    
  18.  /** Convert string buffer on tos to string. 
  19.  */  
  20.  void bufferToString(DiagnosticPosition pos) {  
  21.  callMethod(  
  22.  pos,  
  23.  stringBufferType,  
  24.  names.toString,  
  25.  List.<Type>nil(),  
  26.  false);  
  27.  }  
 

 

这里其实就是将字符串进行了缓存,接着通过调用stringBufferType的toString()方法把StringBuilder中的字符转换为一个字符串对象。

接着我们通过visualvm工具看看上述两个例子运行过程中的内存使用和垃圾回收情况,visualvm工具路径为JDK根目录\bin\jvisualvm.exe

执行使用+操作符进行拼接的监视情况如下


深入分析Java使用+和StringBuilder进行字符串拼接的差异
 

可以看到在运行过程中,虚拟机进行了52871次GC操作共耗费了49.278s,也就是说,运行时间的很大一部分是花在了垃圾回收上。

内存使用情况如下:


深入分析Java使用+和StringBuilder进行字符串拼接的差异
 

可以看到内存的占用大小也在忽上忽下,同样是垃圾回收的表现。

至于第二个例子,因为运行时间仅仅在4毫秒所有,vistalvm还来不及捕捉就执行完毕了,没有捕捉到相关的执行数据。

 

    综上所述,如果在编写代码的过程中大量使用+进行字符串评价还是会对性能造成比较大的影响,但是使用的个数在1000以下还是可以接受的,大于10000的话,执行时间将可能超过1s,会对性能产生较大影响。如果有大量需要进行字符串拼接的操作,最好还是使用StringBuffer或StringBuilder进行。