前言
在开发过程中避免不了与字符串打交道,在使用字符串时,应该对字符串存放位置以及如果高效操作字符串有一个大体的认识。
正题
String对象是不可变的。查看JDK文档你就会发现,String类中每一个看起来会修改String值的方法,实际上都是创建一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。
String对象是不可变的,你可以给一个String对象加以任意多的别。因为String对象具有只读特性,所以指向它的任何引用都不可能改变它的值,因此,也就不会对其他的引用有什么影响。
不可变性会带来一定的效率问题。对String对象重载的“+”操作符就是一个例子。重载的意思是,一个操作符在引用于特定类时,被赋予了特殊的意义(用于String的“+”与“+=”是Java中仅有的两个重载过的操作符,而Java并不允许程序员重载任何操作符)
操作符“+”可以连接String:
public class StringOperation {
public static void main(String[] args) {
String str1 = "hel";
String str2 = str1 + "lo";
System.out.println(str2);
}
}
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 hel
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String lo
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
从以上代码可以看出:编译器自动引用了java.lang.StringBuillder类。虽然我们在源码中并没有使用StringBuilder类,但是编译器却自作主张的使用了它,因为它更高效。现在也许你会觉得可以随意使用String对象,反正编译器会为你自动地优化性能。下面看看另外两种方式生成一个String:
public String implicit(String[] fields){
String result = "";
for(String str : fields){
result += str;
}
return result;
}
public String explicit(String[] fields){
StringBuilder result = new StringBuilder();
for(String str : fields)
{
result.append(str);
}
return result.toString();
}
implicit()方法对应的字节码:
public java.lang.String implicit(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=7, args_size=2
0: ldc #2 // String
2: astore_2
3: aload_1
4: astore_3
5: aload_3
6: arraylength
7: istore 4
9: iconst_0
10: istore 5
12: iload 5
14: iload 4
16: if_icmpge 51
19: aload_3
20: iload 5
22: aaload
23: astore 6
25: new #3 // class java/lang/StringBuilder
28: dup
29: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
32: aload_2
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload 6
38: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: iinc 5, 1
48: goto 12
51: aload_2
52: areturn
从字节码可以看出每一次循环都会创建一个新的StringBuilder对象。
explicit()方法对应的字节码:
public java.lang.String explicit(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=7, 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: aload_1
9: astore_3
10: aload_3
11: arraylength
12: istore 4
14: iconst_0
15: istore 5
17: iload 5
19: iload 4
21: if_icmpge 43
24: aload_3
25: iload 5
27: aaload
28: astore 6
30: aload_2
31: aload 6
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: pop
37: iinc 5, 1
40: goto 17
43: aload_2
44: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: areturn
可以看出不仅循环部分的代码更简短、更简单,而且它只生成了一个StringBuilder对象。因此,当你为一个类编写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理的构造最终的字符串结果。但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果。
接下来看看有关String操作的例子:
代码一
public class StringType {
public static void main(String[] args) {
String str1 = "string";
String str2 = new String("string");
String str3 = str2.intern();
System.out.println(str1 == str2);
System.out.println(str1 == str3);
}
}
对应的class字节码:
Constant pool:
#1 = Methodref #9.#22 // java/lang/Object."<init>":()V
#2 = String #23 // string
#3 = Class #24 // java/lang/String
#4 = Methodref #3.#25 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Methodref #3.#26 // java/lang/String.intern:()Ljava/lang/String;
#6 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#7 = Methodref #29.#30 // java/io/PrintStream.println:(Z)V
#8 = Class #31 // com/java/container/StringType
#9 = Class #32 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 StackMapTable
#17 = Class #33 // "[Ljava/lang/String;"
#18 = Class #24 // java/lang/String
#19 = Class #34 // java/io/PrintStream
#20 = Utf8 SourceFile
#21 = Utf8 StringType.java
#22 = NameAndType #10:#11 // "<init>":()V
#23 = Utf8 string
#24 = Utf8 java/lang/String
#25 = NameAndType #10:#35 // "<init>":(Ljava/lang/String;)V
#26 = NameAndType #36:#37 // intern:()Ljava/lang/String;
#27 = Class #38 // java/lang/System
#28 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#29 = Class #34 // java/io/PrintStream
#30 = NameAndType #41:#42 // println:(Z)V
#31 = Utf8 com/java/container/StringType
#32 = Utf8 java/lang/Object
#33 = Utf8 [Ljava/lang/String;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 (Ljava/lang/String;)V
#36 = Utf8 intern
#37 = Utf8 ()Ljava/lang/String;
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 println
#42 = Utf8 (Z)V
{
public com.java.container.StringType();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: ldc #2 // String string
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #2 // String string
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: aload_2
14: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String;
17: astore_3
18: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_1
22: aload_2
23: if_acmpne 30
26: iconst_1
27: goto 31
30: iconst_0
31: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
34: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
37: aload_1
38: aload_3
39: if_acmpne 46
42: iconst_1
43: goto 47
46: iconst_0
47: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
50: return
}
现在开始看看main方法的执行过程:
0:ldc #2:加载常量池中的第二项(“String”)到栈中
2:astore_1:将栈顶引用型数值存入第二个本地变量
结果#1:str1指向的是常量池中的常量
3:new #3:创建一个String对象,并将其值压入栈顶
6:dup:复制栈顶数值并将复制值压入栈顶
7:ldc #2:加载常量池中的第二项(“String”)到栈中
9:invokevirtual #4:调用实例执行常量池第四项方法
12:astore_2:将栈顶引用型数值存入第三个本地变量
结果#2:str2指向堆中的对象
13:aload_2:将第三个引用类型本地变量推送至栈顶
14:invokevirtual #5:调用实例执行常量池第五项方法
17:astore_3:将栈顶引用型数值存入第四个本地变量
结果#3:str调用intern方法,会将str2中的("string")值复制到常量池中,但如果常量池已经存在该字符串,所以直接返回该字符串的引用,因此str1 = str3
18:getstatic #6:获取指定类的静态域,并将其值压入栈顶
21:aload_1:将第二个引用类型本地变量推送至栈顶
22:aload_2:将第三个引用类型本地变量推送至栈顶
23:if_acmpne 30:比较栈顶两引用型数值,当结果不相等时跳转
26:iconst_1:将int型1推送至栈顶
27:goto 31:无条件跳转
30:iconst_0:将int型0推送至栈顶
31:invokevirtual #7:调用实例执行常量池第七项方法
34:getstatic #6:获取指定类的静态域,并将其值压入栈顶
37:aload_1:将第二个引用类型本地变量推送至栈顶
38:aload_3:将第四个引用类型本地变量推送至栈顶
39:if_acmpne 46:比较栈顶两引用型数值,当结果不相等时跳转
42:iconst_1:将int型1推送至栈顶
43:goto 47:无条件跳转
46:iconst_0:将int型0推送至栈顶
47:invokevirtual #7:调用实例执行常量池第七项方法
50:return:从当前方法返回void
输出结果:false、true
代码二
public class StringType {
public static void main(String[] args) {
String baseStr = "baseStr";
final String baseFinalStr = "baseStr";
String str1 = "baseStr01";
String str2 = "baseStr"+"01";
String str3 = baseStr + "01";
String str4 = baseFinalStr+"01";
String str5 = new String("baseStr01").intern();
System.out.println(str1 == str2);//#3
System.out.println(str1 == str3);//#4
System.out.println(str1 == str4);//#5
System.out.println(str1 == str5);//#6
}
}
常量池:
Constant pool:
#1 = Methodref #15.#28 // java/lang/Object."<init>":()V
#2 = String #29 // baseStr
#3 = String #30 // baseStr01
#4 = Class #31 // java/lang/StringBuilder
#5 = Methodref #4.#28 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = String #33 // 01
#8 = Methodref #4.#34 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #35 // java/lang/String
#10 = Methodref #9.#36 // java/lang/String."<init>":(Ljava/lang/String;)V
#11 = Methodref #9.#37 // java/lang/String.intern:()Ljava/lang/String;
#12 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/PrintStream;
#13 = Methodref #40.#41 // java/io/PrintStream.println:(Z)V
#14 = Class #42 // com/java/container/StringType
#15 = Class #43 // java/lang/Object
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 StackMapTable
#23 = Class #44 // "[Ljava/lang/String;"
#24 = Class #35 // java/lang/String
#25 = Class #45 // java/io/PrintStream
#26 = Utf8 SourceFile
#27 = Utf8 StringType.java
#28 = NameAndType #16:#17 // "<init>":()V
#29 = Utf8 baseStr
#30 = Utf8 baseStr01
#31 = Utf8 java/lang/StringBuilder
#32 = NameAndType #46:#47 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 01
#34 = NameAndType #48:#49 // toString:()Ljava/lang/String;
#35 = Utf8 java/lang/String
#36 = NameAndType #16:#50 // "<init>":(Ljava/lang/String;)V
#37 = NameAndType #51:#49 // intern:()Ljava/lang/String;
#38 = Class #52 // java/lang/System
#39 = NameAndType #53:#54 // out:Ljava/io/PrintStream;
#40 = Class #45 // java/io/PrintStream
#41 = NameAndType #55:#56 // println:(Z)V
#42 = Utf8 com/java/container/StringType
#43 = Utf8 java/lang/Object
#44 = Utf8 [Ljava/lang/String;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 append
#47 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = Utf8 toString
#49 = Utf8 ()Ljava/lang/String;
#50 = Utf8 (Ljava/lang/String;)V
#51 = Utf8 intern
#52 = Utf8 java/lang/System
#53 = Utf8 out
#54 = Utf8 Ljava/io/PrintStream;
#55 = Utf8 println
#56 = Utf8 (Z)V
main函数:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=8, args_size=1
0: ldc #2 // String baseStr
2: astore_1
3: ldc #3 // String baseStr01
5: astore_3
6: ldc #3 // String baseStr01
8: astore 4
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
17: aload_1
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: ldc #7 // String 01
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: astore 5
31: ldc #3 // String baseStr01
33: astore 6
35: new #9 // class java/lang/String
38: dup
39: ldc #3 // String baseStr01
41: invokespecial #10 // Method java/lang/String."<init>":(Ljava/lang/String;)V
44: invokevirtual #11 // Method java/lang/String.intern:()Ljava/lang/String;
47: astore 7
49: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
52: aload_3
53: aload 4
55: if_acmpne 62
58: iconst_1
59: goto 63
62: iconst_0
63: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
66: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
69: aload_3
70: aload 5
72: if_acmpne 79
75: iconst_1
76: goto 80
79: iconst_0
80: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
83: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
86: aload_3
87: aload 6
89: if_acmpne 96
92: iconst_1
93: goto 97
96: iconst_0
97: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
100: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
103: aload_3
104: aload 7
106: if_acmpne 113
109: iconst_1
110: goto 114
113: iconst_0
114: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
117: return
0:ldc #2:加载常量池第二项(“baseStr”)到栈中
2:astroe_1:将栈顶引用型数值存入第二个本地变量,即String baseStr = “baseStr”;
3:ldc #3:加载常量池第三项(“baseStr01”)到栈中
5:astore_3:将栈顶引用型数值存入到第四个本地变量,即String str1 = "baseStr01";
结果#1:str1指向常量池(“baseStr01”)引用
6:ldc #3:加载常量池第三项(“baseStr01”)到栈中
8:astore #4:将栈顶引用型数值存入到第五个本地变量,即String str2= "baseStr01";
结果#2:str2指向常量池(“baseStr01”)引用
10:new #4:创建一个StringBuilder对象,并将其引用值压入栈顶
13:dup:复制栈顶数值并将数值压入栈顶
14:invokevirtual #5:调用实例执行常量池第五项方法,即StringBuilder.<init>方法
17:aload_1:将第二个引用类型本地变量推送至栈顶,即“baseStr”
18:invokevirtual #6:调用实例执行常量池第六项方法,即StringBuilder.append方法
21:ldc #7:加载常量池第七项(“01”)到栈中
23:invokevirtual #6:调用实例执行常量池第六项方法,即StringBuilder.append方法
26:invokevirtual #8:调用实例执行常量池第六项方法,即StringBuilder.toString方法
29:astore #5:将栈顶引用型数值存入到第六个本地变量,即String str3= "baseStr01";
结果#3:str3指向堆对象
31:ldc #3:加载常量池第三项(“baseStr01”)到栈中
33:astore 6:将栈顶引用型数值存入到第七个本地变量,即String str4= "baseStr01";
结果#4:可以看出,对于final字段,编译期直接进项了常量替换,而对于非final字段则在运行时进行赋值处理
35:new #9:创建一个String对象,并将其引用值压入栈顶
38:dup:复制栈顶数值并将数值压入栈顶
39:ldc #3:加载常量池第三项(“baseStr01”)到栈中
41:invokevirtual #10:调用实例执行常量池第十项方法,即String.<init>方法
44:invokevirtual #11:调用实例执行常量池第十一项方法,即String.intern方法
结果#5:调用intern方法,会将“baseStr01”值复制到常量池中,但如果常量池已经存在该字符串,所以直接返回该字符串的引用
输出结果:true、false、true、true
代码三:
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7中运行,会得到一个true和一个false。产生差异的原因是:在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK1.7(以及部分其他虚拟机,例如JRockit)的intern实例不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此放回true。
JDK1.6执行String.intern()方法示意图:
JDK1.7执行String.intern()方法示意图: