java.lang.String作为一个最常用的类,出现在各种各样的编程环境中。而其实现方式的不同可能造成运行效率的大大不同。只有深入了解JVM对String的实现机制,才能更好的利用String。
-
String的不变性
String是一个final类,因此它不能被继承,同时,用以存储字符串内容的char value[]数组是private、final的,因此同样不能改变它的内容。下面是String类的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
那么在JVM中是怎么对String进行更新处理的呢?答案是新建一个String类,将原引用指向这个新String类。
public class Main {
public static String test(String s){
return s.toUpperCase();
}
public static void main(String []args){
String s1 = "asd";
String s2 = test(s1);
System.out.println("s1 is : " + s1 + " and s2 is : " + s2);
}
}
/××
s1 is : asd and s2 is : ASD
×/
s1作为一个拷贝传递给了test函数,返回了一个新的对象引用,而s1本身并没有改变。
但是不变性造成的一个麻烦就是浪费空间,降低效率,如下代码:
String s = "a" + "b" + "c";
如果不考虑JVM所做的优化(直接生成一个字符串"abcd",而不是运行时合成;或优化使用 StringBuilder),那么这句语句在执行的时候就会先生成一个"a",然后"a"和"b"组合生成字符串"ab",然后生成"abc",其效率可想而知(据说java最先版本就是如此)。但是上面这个例子,由于都是不可变的字符串,因此JVM会一次性合成"abc",进行优化。那么合成串中包含变量呢?代码如下:
String a = "adsf";
String s = "a" +"b" +"c" + a;
JVM在处理这段代码的时候就会用到StringBuiler类。
-
StringBuiler
StringBuiler是一个可变的普通类。可以调用append()的方法直接在当前串后面加上新串,而不会生成新的StringBuilder,它是final的,但是,它所实现的AbstractStringBuiler抽象类中,储存值的char[] value不是final的,这点与String不同。
使用类似上面String的代码加以说明,运行后s1和s2的值同时改变了,可见s.append()并不是返回一个新串,而是在引用指向的对象上动手脚。
public class Main {
public static StringBuilder test(StringBuilder s){
return s.append("d");
}
public static void main(String []args){
StringBuilder s1 = new StringBuilder("abc");
StringBuilder s2 = s1;
test(s1);
System.out.println("s1 is : " + s1 + " and s2 is : " + s2);
}
}
/××
s1 is : abcd and s2 is : abcd
×/
回到刚才声明String的代码,
String a = "adsf";
String s = "a" +"b" +"c" + a;
让我们看一看JVM是怎么处理它的,通过javap -c Main命令查看Main的jvm汇编程序:
Compiled from "Main.java"
public class Main {
public Main();
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 adsf
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: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
先说说几个指令的含义:
- ldc:将int、float或String型常量值从常量池中推送至栈顶
- astore_n:将栈顶数值存入当前frame的局部变量数组中指定下标(n)处的变量中,栈顶数值出栈。
- new:创建一个对象,并且其引用进栈
- dup:复制栈顶数值,并且复制值进栈
- invoke...:调用方法
- aload_n:当前frame的局部变量数组中下标为n的引用型局部变量进栈
main函数中,首先直接合成字符串"adsf"放入栈顶,然后存入局部变量s。接下来创建一个StringBuiler类,初始化,接着调用append()方法将abc加入,再次调用append()加入adsf,然后调用toString()输出。
可见在复杂的String增长中,JVM基本上都会对String进行优化,使用StringBuiler。但是这个优化并不是始终最好的,不能认为有JVM就高枕无忧:
public static void main(String []args){
String add[] = "q w e r t y u i o p".split(" ");
String str = "";
for(int i = 0;i < add.length;i++){
str += add[i];
}
}
以上代码在一个循环中,每次为str加上一个String,看看它的实现:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String q w e r t y u i o p
2: ldc #3 // String
4: invokevirtual #4 // Method java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;
7: astore_1
8: ldc #5 // String
10: astore_2
11: iconst_0
12: istore_3
13: iload_3
14: aload_1
15: arraylength
16: if_icmpge 46
19: new #6 // class java/lang/StringBuilder
22: dup
23: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
26: aload_2
27: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: aload_1
31: iload_3
32: aaload
33: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
39: astore_2
40: iinc 3, 1
43: goto 13
46: return
可以看到,在循环体中(13~43),每次循环,都会新建一个StringBuiler对象。显然JVM在此处犯蠢了,因此我们不应该寄希望于JVM进行优化,更好的方法是直接将str声明为StringBuiler:
public static void main(String []args){
String add[] = "q w e r t y u i o p".split(" ");
StringBuilder str = new StringBuilder();
for(int i = 0;i < add.length;i++){
str.append(add[i]);
}
}
这样就只会有一个StringBuiler对象被创建:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String q w e r t y u i o p
2: ldc #3 // String
4: invokevirtual #4 // Method java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;
7: astore_1
8: new #5 // class java/lang/StringBuilder
11: dup
12: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
15: astore_2
16: iconst_0
17: istore_3
18: iload_3
19: aload_1
20: arraylength
21: if_icmpge 38
24: aload_2
25: aload_1
26: iload_3
27: aaload
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: pop
32: iinc 3, 1
35: goto 18
38: return
因此,了解String和StringBuiler的实现,对写出高效的程序很有必要。
-
StringBuffer
有过了解的应该还知道有一个StringBuffer的类,它同样实现了AbstractStringBuiler接口,大部分函数和StringBuiler功能相同,但是区别就在于这些函数前面加了synchronized关键字,保证线程安全。因此其运行速度要比StringBuiler稍微慢一些,但是保证了多线程下数据的准确性。这里就不再多说了。