今天看到有网友在我的博客留言,讨论java中String在进行拼接时使用+和StringBuilder和StringBuffer中的执行速度差异很大,而且之前看的书上说java在编译的时候会自动将+替换为StringBuilder或StringBuffer,但对于这些我都没有做深入的研究,今天准备花一点时间,仔细研究一下。
首先看一下java编译器在编译的时候自动替换+为StringBuilder或StringBuffer的部分,代码如下。
测试环境为win764位系统,8G内存,CPU为 i5-3470,JDK版本为32位的JDK1.6.0_38
第一次使用的测试代码为:
public static void main(String[] args) { // TODO Auto-generated method stub String demoString=""; int execTimes=10000; if(args!=null&&args.length>0) { execTimes=Integer.parseInt(args[0]); } System.out.println("execTimes="+execTimes); long starMs=System.currentTimeMillis(); for(int i=0;i<execTimes;i++) { demoString=demoString+i; } long endMs=System.currentTimeMillis(); System.out.println("+ exec millis="+(endMs-starMs)); }
输入不同参数时的执行时间如下:
C:\>java StringAppendDemo 100 execTimes=100 + exec millis=0 C:\>java StringAppendDemo 1000 execTimes=1000 + exec millis=6 C:\>java StringAppendDemo 10000 execTimes=10000 + exec millis=220 C:\>java StringAppendDemo 100000 execTimes=100000 + exec millis=44267
可以看到,输入的参数为10000和100000时,其执行时间从0.2秒到了44秒。
我们先使用javap命令看一下编译后的代码:
javap –c StringAppendDemo
这里我摘录了和循环拼接字符串有关的那部分代码,具体为:
51: lstore_3 52: iconst_0 53: istore 5 55: iload 5 57: iload_2 58: if_icmpge 87 61: new #5; //class java/lang/StringBuilder 64: dup 65: invokespecial #6; //Method java/lang/StringBuilder."<init>":()V 68: aload_1 69: invokevirtual #8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 72: iload 5 74: invokevirtual #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 77: invokevirtual #10; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 80: astore_1 81: iinc 5, 1 84: goto 55
可以看到,之前的+的确已经被编译为了StringBuilder对象的append方法。通过这里的字节码可以看到,对于每一个+都将被替换为一个StringBuilder而不是我所想象的只生成一个对象。也就是说,如果有10000个+号就会生成10000个StringBuilder对象。具体参看上面字节码的第84行,此处是执行完一次循环以后,再次跳转到55行去执行。
接着,我们把再写一个使用StringBuilder直接实现的方式,看看有什么不一样。
具体代码为:
public class StringBuilderAppendDemo { public static void main(String[] args) { // TODO Auto-generated method stub String demoString=""; int execTimes=10000; if(args!=null&&args.length>0) { execTimes=Integer.parseInt(args[0]); } System.out.println("execTimes="+execTimes); long starMs=System.currentTimeMillis(); StringBuilder strBuilder=new StringBuilder(); for(int i=0;i<execTimes;i++) { strBuilder.append(i); } long endMs=System.currentTimeMillis(); System.out.println("StringBuilder exec millis="+(endMs-starMs)); } }
和上次一样的参数,看看执行时间的差异
C:\>java StringBuilderAppendDemo 100 execTimes=100 StringBuilder exec millis=0 C:\>java StringBuilderAppendDemo 1000 execTimes=1000 StringBuilder exec millis=1 C:\>java StringBuilderAppendDemo 10000 execTimes=10000 StringBuilder exec millis=1 C:\>java StringBuilderAppendDemo 100000 execTimes=100000 StringBuilder exec millis=5
可以看到,这里的执行次数上升以后,执行时间并没有出现大幅度的增加,那我们在看一下编译后的字节码。
51: lstore_3 52: new #5; //class java/lang/StringBuilder 55: dup 56: invokespecial #6; //Method java/lang/StringBuilder."<init>":()V 59: astore 5 61: iconst_0 62: istore 6 64: iload 6 66: iload_2 67: if_icmpge 84 70: aload 5 72: iload 6 74: invokevirtual #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 77: pop 78: iinc 6, 1 81: goto 64
通过字节码可以看到,整个循环拼接过程中,只在56行对StringBuilde对象进行了一次初始化,以后的拼接操作的循环都是从64行开始,然后到81行进行goto 64再次循环。
为了证明我们的推断,我们需要看看虚拟机中是否是这么实现的。
参考代码:http://www.docjar.com/html/api/com/sun/tools/javac/jvm/Gen.java.html
具体的方法,标红的地方就是在语法树处理过程中的一个用来处理字符串拼接“+”号的例子,其他部分进行的处理也类似,我们只保留需要的部分
public void visitAssignop(JCAssignOp tree) { OperatorSymbol operator = (OperatorSymbol) tree.operator; Item l; if (operator.opcode == string_add) { // Generate code to make a string buffer makeStringBuffer(tree.pos()); // Generate code for first string, possibly save one // copy under buffer l = genExpr(tree.lhs, tree.lhs.type); if (l.width() > 0) { code.emitop0(dup_x1 + 3 * (l.width() - 1)); } // Load first string and append to buffer. l.load(); appendString(tree.lhs); // Append all other strings to buffer. appendStrings(tree.rhs); // Convert buffer to string. bufferToString(tree.pos()); }剩余代码已删除。
而具体把+转换为StringBuilder的方法为:
void makeStringBuffer(DiagnosticPosition pos) { code.emitop2(new_, makeRef(pos, stringBufferType)); code.emitop0(dup); callMethod( pos, stringBufferType, names.init, List.<Type>nil(), false); }
看标红出的代码可以知道,此处调用了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方法。具体代码如下
/** Add all strings in tree to string buffer. */ void appendStrings(JCTree tree) { tree = TreeInfo.skipParens(tree); if (tree.getTag() == JCTree.PLUS && tree.type.constValue() == null) { JCBinary op = (JCBinary) tree; if (op.operator.kind == MTH && ((OperatorSymbol) op.operator).opcode == string_add) { appendStrings(op.lhs); appendStrings(op.rhs); return; } } genExpr(tree, tree.type).load(); appendString(tree); } /** Convert string buffer on tos to string. */ void bufferToString(DiagnosticPosition pos) { callMethod( pos, stringBufferType, names.toString, List.<Type>nil(), false); }
这里其实就是将字符串进行了缓存,接着通过调用stringBufferType的toString()方法把StringBuilder中的字符转换为一个字符串对象。
接着我们通过visualvm工具看看上述两个例子运行过程中的内存使用和垃圾回收情况,visualvm工具路径为JDK根目录\bin\jvisualvm.exe
执行使用+操作符进行拼接的监视情况如下
可以看到在运行过程中,虚拟机进行了52871次GC操作共耗费了49.278s,也就是说,运行时间的很大一部分是花在了垃圾回收上。
内存使用情况如下:
可以看到内存的占用大小也在忽上忽下,同样是垃圾回收的表现。
至于第二个例子,因为运行时间仅仅在4毫秒所有,vistalvm还来不及捕捉就执行完毕了,没有捕捉到相关的执行数据。
综上所述,如果在编写代码的过程中大量使用+进行字符串评价还是会对性能造成比较大的影响,但是使用的个数在1000以下还是可以接受的,大于10000的话,执行时间将可能超过1s,会对性能产生较大影响。如果有大量需要进行字符串拼接的操作,最好还是使用StringBuffer或StringBuilder进行。
相关推荐
本文将深入探讨如何在Java中使用"+"运算符进行字符串拼接,并分析其性能影响及与`StringBuffer`、`StringBuilder`之间的区别。 #### 一、"+"运算符进行字符串拼接 在Java中,可以使用"+"运算符来连接两个或多个...
本文主要探讨了在Java中几种不同的字符串拼接方法的性能差异,包括使用操作符`+`、`String.concat()`、`StringBuffer.append()`和`StringBuilder.append()`。以下是对这些方法的详细分析: 1. **字符串拼接操作符 ...
接下来,我们来看Java提供的另外三种字符串拼接方法:`String.concat()`,`StringBuffer.append()` 和 `StringBuilder.append()`。`concat()` 方法虽然比“+”运算符稍微高效一些,但仍然不适合在循环中大量使用。`...
其次,`StringBuilder`和`StringBuffer`类在进行字符串拼接时表现更优,特别是当拼接操作在循环中进行时。与`+`运算符不同,这两个类提供了`append()`方法,可以在不创建新对象的情况下添加字符。`StringBuffer`是...
`StringBuffer`是Java中用于可变字符串操作的类,它提供了线程安全的字符串拼接功能;而“+”运算符则是最直观但可能不是最高效的字符串连接方式,特别是在循环中频繁使用时。 ### 部分内容解析: #### 1. “+”...
它被广泛用于存储文本信息,而关于字符串创建的理解,以及"equals()"方法和"=="运算符的区别,是每个Java开发者都应深入掌握的知识点。 首先,让我们来探讨字符串的创建。在Java中,有多种创建字符串的方式: 1. *...
在给出的代码片段中,通过不同的方式创建了多个`String`对象,并进行了相等性的比较,还测试了字符串拼接的不同方法(包括直接拼接、使用`StringBuffer`等)的执行效率。 #### 字符串创建与比较 1. **字符串常量池...
递归法直观但效率较低,因为涉及到多次函数调用和字符串拼接,可能会导致较大的栈空间消耗。 性能对比: - **数组索引逆序法**:Python的切片操作非常高效,几乎与原始字符串长度成线性关系。C++的双指针技巧时间...
6. **字符串拼接**:在Python中,可以使用`+`运算符或者`join()`方法;在Java中,使用`+`或`StringBuilder/StringBuffer`类。 7. **字符串分割**:`split()`方法是将字符串按照指定的分隔符切割成多个子串,返回一...
Java中的`StringBuffer`和`StringBuilder`类是用于构建和拼接字符串的高效工具,尤其在处理大量字符串连接时,它们的表现优于简单的使用`+`运算符或创建新的`String`对象。这两个类的主要区别在于线程安全性和性能。...
- 未使用`StringBuilder`或`StringBuffer`类进行字符串拼接,这可能导致性能下降。 #### 第二种实现方式: ```java private String extractPingReportBody(String strPingReport) { int index0 = strPingReport....
这相比使用`+`或`StringBuilder`进行字符串拼接,更具有可读性和性能优势。 `StringJoiner`的基本构造方法接受三个参数: 1. **delimiter**:这是在每个字符串元素之间插入的分隔符,默认为空字符串。 2. **prefix...
这些类库提供了大量的工具类和接口,如`String`、`ArrayList`、`HashMap`等,通过阅读源码,我们可以了解到这些常用类和接口的具体实现,例如`StringBuilder`是如何高效地处理字符串拼接的,`ArrayList`在扩容时如何...
- 明确不可变性和字符串池的概念,理解字符串拼接的不同方式(`+`操作符、`StringBuilder`、`StringBuffer`)的性能差异。 **1.2.4 Java关键字** - 掌握常用的关键字如`final`、`static`、`abstract`等的意义和用法...
- **字符串的拼接:** 使用不同的方法进行字符串拼接,并了解性能上的差异。 ##### 1.6 继承和多态 - **继承的概念与作用:** 了解继承的好处,如代码复用等。 - **super关键字:** 掌握super关键字的使用场景。 - ...
- **字符串操作**:String的特点(不可变性),StringBuilder与StringBuffer的区别,以及字符串拼接的最佳实践。 2. **集合框架**: - **List、Set、Queue接口**:ArrayList、LinkedList、HashSet、HashMap等实现...
根据提供的文件信息,这里将对其中提及的关键Java知识点进行详细解释和分析: ### Java最新面试题库中的核心知识点 #### 1....这些知识点对于深入理解Java语言和进行Java面试准备都是非常重要的。
字符串拼接创建对象数量 `Strings = "a" + "b" + "c" + "d";`创建的对象数量取决于字符串常量池的状态,如果所有的字符串都在常量池中,则可能只创建一个新对象。 #### 38. try-catch-finally语句块 在`try`块中...
在Java编程语言中,String、StringBuilder和StringBuffer都是用来处理字符串的类,但它们之间存在显著的区别和使用场景。下面是对这三个类的深入理解和分析。 首先,String类是最常见的字符串对象,它以其不可变性...
5. **字符串操作**:String、StringBuilder和StringBuffer的比较,以及高效字符串拼接方法。 6. **日期时间API**:旧的Date和Calendar与新的java.time包的使用对比。 7. **反射机制**:如何使用反射动态调用方法和...