在Java当中,String类中的每一个看上去会修改String值的方法,实际上都会创建一个全新的String对象,以包含修改后字符串的内容。
String a = "Hello";
a = a + ", World";
代码中第一行的a和第二行的a指向的是两个不同的物理位置。
String对象是不可变的,他具有只读的属性,所以指向它的任何引用都不可能改变他的值,因此也就不会对其他的引用产生影响
String a = "Hello";
String b = a;
a = "World";
System.out.println(a);
System.out.println(b);
运行结果:
World
Hello
可以看到指向a的改变并没有影响到b的值
可变性会带来一定效率上的问题,String对象重载的”+”操作符就是一个例子。
例如String b = "abc " + a + " def " + 55;
这段代码可能是这样工作的:
String可能首先创建"abc "
这个字符串,之后与a
相连接创建"abc " + a
,再与" def "
相连生成新的String对象,以此类推……
public class Concatenation {
public static void main(String...args) {
String a = "Hello";
// 编译器真的会傻傻的生成大量的中间对象吗?
String b = "abc " + a + " def " + 55;
System.out.println(b);
}
}
这种创建方式虽然行得通,但是为了生成最终的String,产生了一大堆需要垃圾回收的中间对象。那么编译器该如何对以上代码进行优化呢?我们使用JDK自带的工具javap来反编译以上代码:
javap -c Concatenation
得到如下的字节码(Bytecode):
λ javap -c Concatenation
警告: 二进制文件Concatenation包含String.Concatenation
Compiled from "Concatenation.java"
public class String.Concatenation {
public String.Concatenation();
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 Hello
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 55
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
}
从第3行可以看出编译器自动引入了java.lang.StringBuider类。虽然在我们的源码中没有使用StringBuider,但是编译器为了优化代码自动使用了该类。
本例中,编译器创建了一个StringBuilder对象,用来构造最终的String,并为每个字符串调用append()方法,最后调用toString(),这样做远比生成一大堆中间类要高效得多。
所以我们是不是就可以随便使用String对象咯,反正编译器会自动帮你优化性能?
答案是否定的。我们来看看编译器能为我们优化到什么地步:
public class WhetherStringBuilder {
public String implicit(String[] args) {
String result = "";
for (int i = 0; i < args.length; i++)
result += args[i];
return result;
}
public String explicit(String[] args) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < args.length; i++)
result.append(args[i]);
return result.toString();
}
}
同样使用javap得到如下字节码:
λ javap -c WhetherStringBuilder
警告: 二进制文件WhetherStringBuilder包含String.WhetherStringBuilder
Compiled from "WhetherStringBuilder.java"
public class String.WhetherStringBuilder {
public String.WhetherStringBuilder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.String implicit(java.lang.String[]);
Code:
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
首先是implicit(),从第8到第35行构成了一个循环体,这个循环体每run一次,就会创建一个新的StringBuilder对象
public java.lang.String explicit(java.lang.String[]);
Code:
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
}
而explicit()方法的字节码就很简单了,自始至终只创建了一个StringBullder,并不断地进行append()操作
注意:类似
append(a + ":" + b)
的语句,编译器会另外创建一个StringBuilder来处理括号内的字符串操作,有兴趣的读者可以自己尝试验证一下。