面试的时候可能常常会问这样一个问题:请说一下String、StringBuilder、StringBuffer的区别,可能不少人会说String若是经过+
(这是加好,后面同理)来拼接字符串时,会建立不少临时变量,性能比较低(网上不少帖子也是这么写的),可是,真的是这样的吗?java
那么String经过+
来拼接字符串时,到底有没有建立临时变量呢?其实,这个问题很简单,只须要经过javap
反编译生成的class文件,看看class文件中String所作的操做就能够了。下面咱们就以《java编程思想》中字符串章节的例子来说解。面试
首先咱们来看下面这段代码:编程
public class Test {
public static void main(String[] args){
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
复制代码
这段代码是比较典型的经过+
来拼接字符串的代码,接下来咱们经过javac Test.java
来编译这段代码,而后经过javap -c Test.class
反编译生成的Test.class
文件。剔除掉一些无关的部分,主要展现了main()
中代码的字节码,因而有了如下的字节码。你会发现很是有意思的东西发生了。segmentfault
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String mango
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 abc
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: ldc #7 // String def
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: bipush 47
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_2
37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
}
复制代码
这里涉及到汇编语言,读者能够网上搜索字节码指令表会有不少,我这里提供一个,读者能够对着表来理解每个指令的含义,这里我不详细展开。每一个指令的后面可能会有//
,//
后面的内容表示指令码操做的对象。细心的读者必定会发现:编译器自动引入了java.lang.StringBuilder(其中java前面的L
表示引用类型,想要详细了解的读者能够看一下《深刻理解Java虚拟机》中类文件结构那一章)。虽然咱们在源代码中没有使用StringBuilder,可是编译器却自做主张的使用了它,由于它更高效。数组
看上面的字节码你会发现,编译器建立StringBuilder对象以后,对+
号相连的每个字符串使用append()
方法来拼接,总计调用了四次,最后调用toString()方法生成结果。(注:读者感兴趣的话能够用StringBuilder来替换上面的代码,经过javac
javap
从新编译,而后你会发现main()
方法中生成的字节码是同样的)。bash
经过上面的例子咱们发现,当咱们经过+
来拼接字符串时,编译器会自动替咱们优化成StringBuilder来拼接,并不会形成网上所说的建立临时变量,速度变慢这些缺点。(注:因为StringBuidler是在jdk5.0以后引入的,因此jdk5.0以前是经过StringBuffer来拼接的,感兴趣的读者能够自行验证)。app
如今,咱们确定会很开心,既然编译器都替咱们优化了,那咱们是否是能够随意使用String
了呢(想一想都开心)。哈哈,不要高兴得太早,由于有时候编译器的优化可能并非你想要的结果。让咱们来看下面这段代码:jvm
下面这段程序采用两种方式生成一个String:方法一使用多个String对象;方法二代码中使用了StringBuidler。性能
public class Test {
public String testString(String[] fields) {
String result = "";
for (int i = 0; i < fields.length; i++) {
result += fields[i];
}
return result;
}
public String testStringBuilder(String[] fields){
StringBuilder result = new StringBuilder();
for (int i = 0; i<fields.length; i++){
result.append(fields[i]);
}
return result.toString();
}
}
复制代码
上面代码中的两个方法执行相似,都是传入字符串数组,而后经过for
循环将数组字符串拼接起来,区别是第一个方法使用String
来拼接,第二个方法使用StringBuilder
来拼接。而后咱们仍是经过javap
来反编译这段代码,剔除无关部分,会看到两个方法的字节码。优化
首先是testString()
方法的字节码:
public java.lang.String testString(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: ldc #2 // String
2: astore_2
3: iconst_0
4: istore_3
5: iload_3
6: aload_1
7: arraylength
8: if_icmpge 38
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_2
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: iload_3
24: aaload
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_2
32: iinc 3, 1
35: goto 5
38: aload_2
39: areturn
复制代码
这里读者看一下第8行中的if_icmpge
,对照字节码指令表会发现这个指令就是for循环中比较i
值等于某个值时进入循环,后面的38表示在38行跳出循环,这里的循环体是第8
行到第35
行。第35行的意思是:返回循环体的起始点(第5
行)。而后咱们看循环体(第8
行到第35
行)中第11行,是一个new
指令,这个太熟悉了,就是建立对象。可是它竟然是在循环体内部,这就意味着每循环一次,就要建立一个新的StringBuilder
对象。这显然不能接受。
那咱们再看一下testStringBuilder()
的字节码:
public java.lang.String testStringBuilder(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: iload_3
11: aload_1
12: arraylength
13: if_icmpge 30
16: aload_2
17: aload_1
18: iload_3
19: aaload
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 3, 1
27: goto 10
30: aload_2
31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: areturn
复制代码
咱们能够看到,不只循环体部分的代码更简短、更简单了,并且new
只在刚开始调用了一次,说明只生成了一个StringBuilder对象。
因此,当你为一个类编写toString()
方法时,若是字符串操做比较简单,那就能够信赖编译器,它会为你合理的构造最终的字符串结果。可是,若是你要在toString()
方法中使用循环,那么你就须要本身建立一个StringBuilder对象。固然,当你拿不定主意的时候,那么你随时能够经过javap来分析你的程序。
留一个问题:枚举你们应该都知道,可是你知道它在jvm中究竟是怎么执行的吗?(思路:其实要想知道它的原理很简单,你一样能够写一段枚举代码,而后经过javap
反编译这段代码,你会有一种豁然开朗的感受。)
-《java编程思想》