1. 程式人生 > >深入瞭解String,StringBuffer和StringBuilder三個類的異同

深入瞭解String,StringBuffer和StringBuilder三個類的異同

Java提供了三個類,用於處理字串,分別是String、StringBuffer和StringBuilder。其中StringBuilder是jdk1.5才引入的。

這三個類有什麼區別呢?他們的使用場景分別是什麼呢?

本文的程式碼是在jdk12上執行的,jdk12和jdk5,jdk8有很大的區別,特別是String、StringBuffer和StringBuilder的實現。

jdk5和jdk8中String類的value型別是char[],到了jdk12,value型別變為byte[]。

jdk5、JDK6中的常量池是放在永久代的,永久代和Java堆是兩個完全分開的區域。

到了jdk7及以後的版本,

我們先來看看這三個類的原始碼。

String類部分原始碼:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    @Stable
    private final byte[] value;
    
    public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;
    }
    public native String intern();

String類由final修飾符修飾,所以String類是不可變的,物件一旦建立,不能改變。

String類中有個value的位元組陣列成員 變數,這個變數用於儲存字串的內容,也是用final修飾,一旦初始化,不可改變。

java提供了兩種主要方式建立字串:

//方式1
String str = "123";
//方式2
String str = new String("123");

java虛擬機器規範中定義字串都是儲存在字串常量池中,不管是用方式1還是方式2建立字串,都會從去字串常量池中查詢,如果已經存在,直接返回,否則建立後返回。

java編譯器在編譯java類時,遇到“abc”,“hello”這樣的字串常量,會將這些常量放入類的常量區,類在載入時,會將字串常量加入到字串常量池中。

含有表示式的字串常量,不會在編譯時放入常量區,例如,String str = "abc" + a

常量池的最大作用是共享使用,提高程式執行效率。

看看下面幾個案例。

案例1:

1  String str1 = "123";
2  String str2 = "123";
3  System.out.println(str1 == str2);

上面程式碼執行的結果為true。

執行第1行程式碼時,現在常量池中建立字串123物件,然後賦值給str1變數。

執行第2行程式碼時,發現常量池已經存在123物件,則直接將123物件的地址返回給變數str2。

str1和str2變數指向的地址一樣,他們是同一個物件,因此執行的結果為true。

從圖中可以看出,str1使用””引號(也是平時所說的字面量)建立字串,在編譯期的時候就對常量池進行判斷是否存在該字串,如果存在則不建立直接返回物件的引用;如果不存在,則先在常量池中建立該字串例項再返回例項的引用給str1。

案例2:

1  String str1 = new String("123");  
2  String str2 = new String("123");
3  String str3 = new String(str2);
4  System.out.println((str1==str2));  
5  System.out.println((str1==str3));

6  System.out.println((str3==str2));  

上面程式碼執行的結果是

false
false
false

從上圖可以看出,執行第1行程式碼時,建立了兩個物件,一個存放在字串常量池中,一個存在與堆中,還有一個物件引用str1存放在棧中。

執行第2行程式碼時,字串常量池中已經存在“123”物件,所以只在堆中建立了一個字串物件,並且這個物件的地址指向常量池中“123”物件的地址,同時在棧中建立一個物件引用str2,引用地址指向堆中建立的物件。

執行第3行程式碼時,在堆中建立一個字串物件,這個物件的記憶體地址指向變數str2所執向的記憶體地址。

通過new方式建立的字串物件,都會在堆中開闢一個新記憶體空間,用於儲存常量池中的字串物件。

對於物件而言,==操作是用於比較兩個獨享的記憶體地址是否一致,所以上面的程式碼執行的結果都是false。

案例3:

//這行程式碼編譯後的效果等同於String str1 = "abcd";
String str1 = "ab" + "cd";  
String str2 = "abcd";   
System.out.println((str1 == str2)); 

上面程式碼執行的結果:true。

使用包含常量的字串連線建立的也是常量,編譯期就能確定了,類載入的時候直接進入字串常量池,當然同樣需要判斷字串常量池中是否已經存在該字串。

案例4:

String str2 = "ab";  //1個物件  
String str3 = "cd";  //1個物件                                         
String str4 = str2 + str3 + “1”;                                        
String str5 = "abcd1";    
System.out.println((str4==str5)); 

上面程式碼執行的結果:false。

當使用“+”連線字串中含有變數時,由於變數的值是在執行時才能確定。

如果使用的jdk8以前版本的虛擬機器,在拼接字串時,會在jvm堆中生成StringBuilder物件,呼叫append方法拼接字串,最後呼叫StringBuilder的toString方法在jvm堆中生成最終的字串物件。

通過檢視位元組碼就可以知道jdk8之前版本的"+"拼接字串時通過StringBuilder實現的。通過檢視位元組碼就可以知道,如下圖所示:

而如果使用的是jdk9以後版本的虛擬機器,則是呼叫虛擬機器自帶的InvokeDynamic拼接字串,並且儲存在堆中。位元組碼如下所示:

str4的物件在字串常量池中,str5的物件在堆中,所以他們的不是同一個物件,所以返回的結果是false。

案例5:

String s5 = new String(“2”) + new String(“3”);

和案例4一樣,因為new String("2")建立字串,也是在執行時才能確定物件記憶體地址,和案例4一樣。

案例6:

final String str1 = "b";  
String str2 = "a" + str1;  
String str3 = "ab";  
System.out.println((str2 == str3)); 

上面程式碼執行的結果為true。

str1是常量變數,在編譯期就確定,直接放入到字串常量池中,上面的程式碼效果等同於:

String str2 = "a" + "b";
String str3 = "ab";
System.out.println((str2 == str3));

呼叫String類的intern()方法,會將堆中的字串例項放入到字串常量池中。

案例7:

String str2 = "ab";
String str3 = "cd";
String str4 = str2 + str3 + "1";
str4.intern();
String str5 = "abcd1";
System.out.println((str4==str5));

上面程式碼執行的結果:true。呼叫了str4.intern()方法後,將str4放入到字串常量池中,和str5是同一個例項。

StringBuffer部分原始碼:

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
{

StringBuilder部分原始碼:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{

可見StringBuffer和StringBuilder都繼承了AbstractStringBuilder類。

AbstractStringBuilder類原始碼:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    byte[] value;

AbstractStringBuilder也有一個位元組陣列的成員變數value,這個變數用於儲存字串的值,這個變數不是用final修飾,所以是可以改變的,這個是和String的最大區別。

在呼叫append方法的時候,會動態增加位元組陣列變數value的大小。

StringBuffer和StringBuilder功能是一樣的,都是為了提高java中字串連線的效率,因為直接使用+進行字串連線的話,jvm會建立多個String物件,因此造成一定的開銷。AbstractStringBuilder中採用一個byte陣列來儲存需要append的字串,byte陣列有一個初始大小,當append的字串長度超過當前char陣列容量時,則對byte陣列進行動態擴充套件,也即重新申請一段更大的記憶體空間,然後將當前bute陣列拷貝到新的位置,因為重新分配記憶體並拷貝的開銷比較大,所以每次重新申請記憶體空間都是採用申請大於當前需要的記憶體空間的方式,這裡是2倍。

StringBuffer和StringBuilder最大的區別是StringBuffer是執行緒安全,而StringBuilder是非執行緒安全的,從它們兩個類的原始碼就可以知道,StringBuffer類的方法前面都是synchronized修飾符。

String一旦賦值或例項化後就不可更改,如果賦予新值將會重新開闢記憶體地址進行儲存。

而StringBuffer和StringBuilder類使用append和insert等方法改變字串值時只是在原有物件儲存的記憶體地址上進行連續操作,減少了資源的開銷。

總結:
1、頻繁使用“+”操作拼接字元時,換成StringBuffer和StringBuilder類的append方法實現。

2、多執行緒環境下進行大量的拼接字串操作使用StringBuffer,StringBuffer是執行緒安全的;

3、單執行緒環境下進行大量的拼接字串操作使用StringBuilder,StringBuilder是執行緒不安全的。