- 海之寻趣
- Ranler
- 2013-04-27 12:43
- CC BY-NC-SA
这次看一下String和常量池相关的操作。
ldc
指令,把运行时常量池(runtime constant pool)中的元素压入操作栈。
压入元素的类型由常量池中元素类型决定:
- 如果常量池中的元素是int/float类型,那么压入的类型是float/int
- 如果是String类型的reference,那么压入其String对象的reference
- 如果是class类型的symbolic reference,那么压入其class对象的reference
- 如果是method type/handler的symbolic reference,那么压入其MethodType/MethodHandle对象的reference
可以看到压入的共两种类型:数值类型或reference类型
Example 2.1
又是一个经典笔试题:
void f1() {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s3 == s4);
}
void f1();
Code:
0: ldc #7 // String abc;常量池中取出其ref
2: astore_1 // (String s1 = "abc";)
3: ldc #7 // String abc;常量池中取出其ref
5: astore_2 // (String s2 = "abc";)
6: new #8 // class java/lang/String;创建新的对象并返回ref
9: dup
10: ldc #7 // String abc
12: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3 // (String s3 = new String("abc");)
16: new #8 // class java/lang/String;创建新的对象并返回ref
19: dup
20: ldc #7 // String abc
22: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
25: astore 4 // (String s4 = new String("abc"));
27: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: aload_2
32: if_acmpne 39 // (s1 == s2;),比较了其引用值
35: iconst_1
36: goto 40
39: iconst_0
40: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
43: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_3
47: aload 4
49: if_acmpne 56 // (s3 == s4;),比较了其引用值
52: iconst_1
53: goto 57
56: iconst_0
57: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
60: return
“123”和“456”作为当前类的常量池中成员,在class加载到JVM是已经有对象了。 因此s1,s2分别获得了它们的ref。
只要有new
指令就会在heap中申请内存,从而有一个新的对象实例,新的ref。
因此,这段程序的输出是:
true
false
Example 2.2
void f2() {
String s1 = "12" + "3";
String s2 = "456";
String s3 = s1 + s2;
String s4 = s1 + s2 + s3;
}
void f2();
Code:
0: ldc #12 // String 123
2: astore_1 // (String s1 = "12" + "3";),可以看出编译阶段可以优化字符串
3: ldc #13 // String 456
5: astore_2 // (String s1 = "456";)
6: new #14 // class java/lang/StringBuilder
9: dup
10: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3 // (String s3 = s1 + s2;)
25: new #14 // class java/lang/StringBuilder
28: dup
29: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V
32: aload_1
33: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload_2
37: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: aload_3
41: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: astore 4 // (String s4 = s1 + s2 + s3;)
49: return
每个String对象保存的字符串是不会改变的。 改变String只会新建一个新的String对象。
上段代码可以看出,对于通过+
来进行多个字符串的拼接操作,
Oracle JDK是通过StringBuilder来实现的,
这也是一种高效的做法。
相比于StringBuffer,StringBuilder也免去了sync的代价。
但是这里并没有复用StringBuilder,
每个子符串连接的Java语句都要new一个新的StringBuilder对象,
这也是这种+
操作耗时的地方。
Example 2.3
这个例子看看String.intern()。
void f3() {
String s1 = "123";
String s2 = new String("123");
System.out.println(s1 == s2);
System.out.println(s1.intern() == s2.intern());
System.out.println(s1 == s1.intern());
System.out.println(s2 == s2.intern());
System.out.println(s1 == s2.intern());
}
指令码就不贴了,和上面没什么区别。
String.intern()的实现通过JNI由本地代码实现的, 其功能是在字符串常量池中查找与当前字符串字面值相等的字符串, 返回其在字符串常量池中的引用。 如果不存在,则加入后返回。 因此调用String.intern()可能会扩充字符串常量池。
public naive String intern();
字面值相同的字符串调用intern()都会获得同一字符串对象的引用。
这个页面里的测试说明intern()的调用是及其昂贵的, 大概是O(n)的算法复杂度,n是字符串常量池的大小。
这段代码的结果是:
false
true
true
false
true
s1开始就获得了字符串常量池中的对象,因此s1和s1.intern()都是同一对象的引用值, 结果相等。
Example 2.4
最后比较下StringBuilder复用和·+·直接拼接的效率:
public class TestStrBuilder {
static String s1 = "1";
static String s2 = "2";
static String s3 = "3";
static String s4 = "4";
static String s5 = "5";
static String s6 = "6";
public static void main(String[] args) {
f1(10000000);
f2(10000000);
f1(100000000);
f2(100000000);
}
public static void f1(long time) {
long cost = System.nanoTime();
for (long i = 0; i < time; i++) {
String s = s1 + s2 + s3 + s4 + s5 + s6 + s5 + s4 + s3 + s2 + s1;
}
cost = System.nanoTime() - cost;
System.out.println(cost / 1000000);
}
public static void f2(long time) {
StringBuilder sb = new StringBuilder();
long cost = System.nanoTime();
for (long i = 0; i < time; i++) {
sb.append(s1).append(s2).append(s3).append(s4).append(s5).append(s6);
sb.append(s5).append(s4).append(s3).append(s2).append(s1);
String s = sb.toString();
sb.delete(0, sb.length());
}
cost = System.nanoTime() - cost;
System.out.println(cost / 1000000);
}
}
在OpenJDK 1.7.0_21 64bit下结果是:
1465
1230
13044
10681
大概复用StringBuilder时效率提高了17%。