String,StringBulider和StringBuffer的区别

weblog 精帖
240

String类的实现及其不可变性

        对于String类的实现从源码中可以看出,String类的底层维护着一个final修饰的char数组,用来储存字符。并且除了hash这个属性其它属性都声明为final,从而确保了String类的不可变性。

        但是String类为什么要设计成不可变的呢?众所周知java中String类型可以说是一个非常重要的类,而且在应用程序中经常会大量使用。但是如此大量频繁的创建字符对象又会极大的影响程序性能,所以jvm为了提高性能和减小内存开销,对字符串常量初始化的时候做了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区。
  • 创建字符串常量时,首先检查字符串常量池是否存在该字符串。
  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入常量池中。

        但是这样一优化的话,可能有多个对象引用同一个常量池字中的子符串,所以为了安全性考虑,String类型需要设计成不可变的。当然肯定还有其他的原因,比如效率更高等,在此不一一叙述。

StringBuilder的实现以及和String的区别

StringBuilder的实现

StringBuilder类继承了AbstractStringBuilder类,在AbstractStringBuilder中维护着一个char数组,用来储存字符。

        从源码可以看出来,AbstractStringBuilder中的char数组和String类中的char数组区别是AbstractStringBuilder类中的数组没有final修饰,也就是说StringBuilder对象是可变的,当然StringBuilder也提供了一系列的方法来操作此字符串。比如最常用的apped方法。

已经有了String类,为什么还需要StringBuilder类呢?

        一般在应用程序中经常又会出现有对字符串的操作,而在介绍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是两个对象,地址显然不同,至于产生三个对象,应该也好理解,常量池中一个,堆内存中两个。

分析到此结束。

猜你喜欢