String,StringBulider和StringBuffer的区别
String类的实现及其不可变性
对于String类的实现从源码中可以看出,String类的底层维护着一个final修饰的char数组,用来储存字符。并且除了hash这个属性其它属性都声明为final,从而确保了String类的不可变性。
但是String类为什么要设计成不可变的呢?众所周知java中String类型可以说是一个非常重要的类,而且在应用程序中经常会大量使用。但是如此大量频繁的创建字符对象又会极大的影响程序性能,所以jvm为了提高性能和减小内存开销,对字符串常量初始化的时候做了一些优化。
- 为字符串开辟一个字符串常量池,类似于缓存区。
- 创建字符串常量时,首先检查字符串常量池是否存在该字符串。
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入常量池中。
但是这样一优化的话,可能有多个对象引用同一个常量池字中的子符串,所以为了安全性考虑,String类型需要设计成不可变的。当然肯定还有其他的原因,比如效率更高等,在此不一一叙述。
StringBuilder的实现以及和String的区别
StringBuilder类继承了AbstractStringBuilder类,在AbstractStringBuilder中维护着一个char数组,用来储存字符。
从源码可以看出来,AbstractStringBuilder中的char数组和String类中的char数组区别是AbstractStringBuilder类中的数组没有final修饰,也就是说StringBuilder对象是可变的,当然StringBuilder也提供了一系列的方法来操作此字符串。比如最常用的apped方法。
一般在应用程序中经常又会出现有对字符串的操作,而在介绍String类的实现的时候已经说过,String类是不可变的,所以在操作字符串的时候会不断产生临时对象。这样的话如果操作字符串比较频繁则对性能还是会有较大影响。所以为了解决这一问题StringBuilder就横空出世,StringBuilder对象在操作字符串的过程中不会产生临时对象,它就像一个缓存,储存着所有append进来的所有字符,在最后执行toString方法的时候才产生一个String对象。
举个例子,如有下面两个方法test和test2:
public class Test3 {
public void test() {
String a="a";
a+="b";
a+="c";
a+="d";
a+="e";
}
public void test2() {
StringBuilder a=new StringBuilder("a");
a.append("b");
a.append("c");
a.append("d");
a.append("e");
}
}
最终的结果都是为了得到字符串"abcde",我们反编译一下它的字节码看一下:
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // String a
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 b
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_1
23: new #3 // class java/lang/StringBuilder
26: dup
27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
30: aload_1
31: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc #8 // String c
36: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_1
43: new #3 // class java/lang/StringBuilder
46: dup
47: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
50: aload_1
51: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: ldc #9 // String d
56: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
59: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
62: astore_1
63: new #3 // class java/lang/StringBuilder
66: dup
67: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
70: aload_1
71: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
74: ldc #10 // String e
76: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
82: astore_1
83: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 23
line 8: 43
line 9: 63
line 10: 83
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #3 // class java/lang/StringBuilder
3: dup
4: ldc #2 // String a
6: invokespecial #11 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: ldc #6 // String b
13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: pop
17: aload_1
18: ldc #8 // String c
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: aload_1
25: ldc #9 // String d
27: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: pop
31: aload_1
32: ldc #10 // String e
34: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
37: pop
38: return
LineNumberTable:
line 13: 0
line 14: 10
line 15: 17
line 16: 24
line 17: 31
line 18: 38
从两者的字节码中可以看出,String类在做变量拼接的时候,其实jvm底层实现是new了一个StringBuilder然后执行append方法拼接,从字节码中可以看出,test方法过程中一共new了四个临时对象,而test2方法整个过程只new了一个对象(注意,字符'a','b','c','d','e'已经再常量池中存在),另外从字节码执行的数量上来说,test2方法执行的字节码数量更少。所以在大量操作字符串的时候用StringBuilder能够提升性能。
StringBuffer和StringBuilder的区别
事实上StringBuffer和StringBuilder的唯一的区别就是StringBuffer是线程安全的,而StringBuilder是非线程安全的,StringBuffer把所有修改数据的方法都加上了synchronized。其他两者并没有什么区别。只是在很多情况下,字符串操作都是在单线程情况下,所以在多数情况下StringBuilder还是比较常用,毕竟StringBuffer保证了线程安全是需要性能的代价的。
字符串拼接都会产生临时对象吗?
有些同学可能看到上面说字符串拼接会产生临时对象,似乎有些心慌,暂且大可不必,对于上面的问题,答案是否定的。在一些没有变量的字符串拼接时是不会产生临时对象的。例如:
public class Test3 {
public void test() {
String a="a"+"b"+"c"+"d"+"e";
}
}
对于像上面没有变量的字符产拼接时,jvm虚拟机会帮我们进行优化,也就是说代码编译阶段就直接合成"abcde"了,从字节码的常量池中可以看出(#13处)。
Constant pool:
#1 = Methodref #4.#12 // java/lang/Object."<init>":()V
#2 = String #13 // abcde
#3 = Class #14 // com/dzqc/yx/string/Test3
#4 = Class #15 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 test
#10 = Utf8 SourceFile
#11 = Utf8 Test3.java
#12 = NameAndType #5:#6 // "<init>":()V
#13 = Utf8 abcde
#14 = Utf8 com/dzqc/yx/string/Test3
#15 = Utf8 java/lang/Object
{
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String abcde
2: astore_1
3: return
LineNumberTable:
line 5: 0
line 6: 3
}
问题讨论
public void test3() {
String x="a";
String y="a";//常量池中存在a
System.out.println(x==y);//true
//所以一共创建了一个对象(常量池中)
}
根据jvm对字符串的优化规则可以看出,初始化x的时候把字符串"a"放进常量池,初始化y的时候常量池中已经存在"a",所以返回其引用,两者引用的是同一对象,所以地址是相同的。
public void test4() {
String x="a";//常量池中
String y=new String("a");//a对象已经在常量池中存在,但是new String("a")又会在堆内存中创建一个对象。所以这段代码一共创建了两个对象
System.out.println(x==y);//false
//所以一共创建了两个对象
}
对象x引用的是常量池中的对象,y引用的是堆内存中的对象,所以地址明显不同。
public void test5() {
String x=new String("a");//a对象已经再常量池中存在,new String("a") 再堆中创建一个对象
String y=new String("a");//new String("a") 再堆中创建一个对象
System.out.println(y==x);//false
//所以一共创建了三个对象
}
x,和y是两个对象,地址显然不同,至于产生三个对象,应该也好理解,常量池中一个,堆内存中两个。
分析到此结束。