前言
面试的时候可能经常会问这样一个问题:请说一下String、StringBuilder、StringBuffer的区别,可能很多人会说String如果通过+
(这是加好,后面同理)来拼接字符串时,会创建很多临时变量,性能比较低(网上很多帖子也是这么写的),但是,真的是这样的吗?
字符串的拼接
那么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()
中代码的字节码,于是有了以下的字节码。你会发现非常有意思的东西发生了。
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()
方法中生成的字节码是一样的)。
结论
通过上面的例子我们发现,当我们通过+
来拼接字符串时,编译器会自动替我们优化成StringBuilder来拼接,并不会造成网上所说的创建临时变量,速度变慢这些缺点。(注:由于StringBuidler是在jdk5.0之后引入的,所以jdk5.0之前是通过StringBuffer来拼接的,感兴趣的读者可以自行验证)。
延伸
现在,我们肯定会很开心,既然编译器都替我们优化了,那我们是不是可以随意使用String
了呢(想想都开心)。哈哈,不要高兴得太早,因为有时候编译器的优化可能并不是你想要的结果。让我们来看下面这段代码:
下面这段程序采用两种方式生成一个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编程思想》
今天的文章你真的了解String和StringBuilder吗分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18285.html