1. 程式人生 > 其它 >String、StringBuilder和StringBuffer

String、StringBuilder和StringBuffer

JVM(Java虛擬機器)

學習String類前,先了解一下JVM,也稱為Java虛擬機器。

JVM記憶體分有幾大區域,其中,常見有堆、桟、方法區、常量池。

堆是執行時資料區,類通過new指令建立的物件會在堆記憶體裡分配空間。堆記憶體的資料是由java垃圾回收器自動回收。堆的優勢是可以動態地分配記憶體大小缺點是,由於要在執行時動態分配記憶體,存取速度較慢。

桟是存放一些基本型別的變數資料和物件的引用。優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。


String

好的,大概瞭解了JVM後來學習String。

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

String是java中代表字串的類。java中所有的字串面量值都由此類實現,它被宣告為 final,因此它不可被繼承

private final char value[];

查閱底層程式碼,String內部使用了陣列來儲存資料,這個陣列由final修飾,當陣列初始化後不能再引用其它陣列,也確保了String不可變

建立字串物件

兩種方式

  1. 直接賦值,在字串常量池建立了一個物件

    String str = "che";
    
  2. 通過構造方法,建立字串物件

    String str = new String("che");
    

先來看一下程式

String chen1 = "chen";
String chen2 = "chen";
String chen3 = "chen!!";
String newChen1 = new String("chen");
String newChen2 = new String("chen");
System.out.println(newChen2 == chen1);
System.out.println(newChen1 == newChen2);
System.out.println(chen1 == chen2);
System.out.println(chen1.equals(newChen2));
/*
true
false
true
true

Process finished with exit code 0
*/

兩種建立分式的區別

  • 從記憶體上分析。
    1. 直接賦值的方式。先查詢字串常量池中有沒有s1要建立的物件,沒有則在常量池中建立物件“chen”,然後到s2定義同樣的字串物件時,還會去常量池中找著是否已經有該物件存在,有則把物件的引用例項共享給s2。以上s1、s2在字串常量池中只建立了一個物件,因為程式碼中還沒有出現new所以沒有在堆裡建立物件
    2. 通過new建立字串物件。首先會先去字串常量池中查詢有沒有“chen”的例項引用,有則把該引用共享給堆中的new String(),並把堆中的引用返回到棧中對應的資料,然後壓棧。以上str1、str2在字串常量池有對應的物件時,只在堆中建立了兩個物件,並沒有在常量池中建立物件

字串常量池不會存在兩個相同的字串


Q&A

Q1:Hash table Entry

雜湊表條目,是字串常量池底層實現的一種,用於記錄字串常量池中的資料,我們從常量池中獲取字串,實際是從雜湊表條目中獲取對應的條目值

Q2:位元組碼檔案指令

此處參考的文獻: Java字串字面量是何時進入到字串常量池中的Java 中new String("字面量") 中 "字面量" 是何時進入字串常量池的?

以下是上列程式編譯後的部分位元組碼指令,通過執行javap -c FileClass對class檔案反編譯。或者javap -v FileClass可以更清楚知道常量池的編號對應的資料

public class string_base.TestBase {
  public string_base.TestBase();
    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 chen
       2: astore_1
       3: ldc           #2                  // String chen
       5: astore_2
       6: ldc           #3                  // String chen!!
       8: astore_3
       9: new           #4                  // class java/lang/String
      12: dup
      13: ldc           #2                  // String chen
      15: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      18: astore        4
      20: new           #4                  // class java/lang/String
      23: dup
      24: ldc           #2                  // String chen
      26: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      29: astore        5

解析:

main方法:

0-8行,ldc將常量池中的常量值載入到運算元棧;astore_indexbyte將棧頂引用型別值儲存到區域性變數indexbyte中。

9-29行,new建立一個String物件;dup複製棧頂一個字長的資料,將複製後的資料壓棧;ldc將常量池中的常量值載入到運算元棧;invokespecial用於呼叫特殊的方法,如例項初始化方法、私有方法和父類方法;astore_indexbyte將棧頂引用型別值儲存到區域性變數indexbyte中。

注:ldc 在常量池中沒有對應的常量值時JVM會在常量池中建立該常量值物件。ldc後面的#index是指在常量池中的編號

所以,建議在日常開發中,儘量使用直接賦值的方式去建立String物件,這樣可以節省一部分空間。

雖然堆記憶體的垃圾會有垃圾回收器去回收,但垃圾回收器是隨機去回收的,我們不能讓回收器立即回收某個不再使用的物件,但可以顯示的表明那個物件不再使用了建議垃圾回收器去回收,但它還是不會立即回收。

Q3:==與equals比較的區別

==在對字串比較的時候,對比的是記憶體地址,而equals比較的是字串內容,在開發的過程中, equals()通過接受引數,可以避免空指向。對空指標的物件呼叫方法也是一件錯誤的事,因為他沒有指向具體的例項,所以其中包含的方法無從得知


方法

返回字串的長度

public int length() { return value.length; }

底層是返回字元陣列的長度

返回某個字元在此字串上出現的索引

//返回變數ch在此字串中第一次出現的索引
public int indexOf(int ch)
//返回變數ch在此字串中最後一次出現的索引
public int lastIndexOf(int ch)

將指定的型別值轉換為字串

public static String valueOf(Object obj)

可以是任何型別,底層是在呼叫toString方法

將字串轉換為大小寫

//轉小寫
public String toLowerCase()
//轉大寫
public String toUpperCase()

根據JVM的預設語言環境轉換

用指定的字元替換掉字串中的某個字元

public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)

用指定的字面替換序列替換此字串中與目標字面序列相匹配的每個子串。CharSequence介面被String等類實現。

根據引數分割字串

public String[] split(String regex)
public String[] split(String regex, int limit)

根據regex分割字串,也可以根據limit分割成多少個子串,返回一個字串數值

將字串從指定索引擷取返回索引後面的字串

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

判斷字串是否以指定的子字串開始或結束

public boolean startsWith(String prefix)
public boolean endsWith(String suffix)

判斷字串是否包含指定子字串

public boolean contains(CharSequence s)

判斷字串長度是否為0

public boolean isEmpty()

判斷字串與指字串內容是否相同

public boolean equals(Object anObject)

String重寫了equals方法,還有比較字串內容但忽略大小寫的,equalsIgnoreCase

拼接字串

java允許使用+號連線兩個字串,如果與非字串的值進行拼接時,非字串的值會被轉換成字串

public static String join(CharSequence delimiter, CharSequence... elements)

多字串拼接時,可以用join靜態方法,引數delimiter是用指定的定界符分隔這些字串

String不可變

參考文獻:

Why String is immutable in Java?

《Effective Java》中對於不可變物件的定義是:物件一旦被建立後,物件所有的狀態及屬性在其生命週期內不會發生任何變化

當我們嘗試對一個String物件再次賦值,String會新建立一個物件,舊物件還存在,但沒有被引用。此時,記憶體中就會存在兩個物件。

String str = "s1";
str = "s2";

String的不可變不僅僅是因為底層陣列被final修飾,從而無法被修改。

  • 底層char陣列被private修飾,且內部沒有對外提供修改陣列的方法,所以外界沒有有效的手段去改變它

  • String被final修飾,避免被繼承破壞

  • 在String的中,避免了去修改char陣列的程式碼,涉及對char陣列的操作都會重新去建立一個物件

為什麼要不可變

  1. 如果程式碼中出現了大量頻繁的建立字串,可以提高效能和減少記憶體開銷。建立字串時,首先檢查字串常量池中是否存在該字串。存在,則返回該引用例項;不存在,則例項化該字串放入池中,返回引用例項
  2. 為了安全。不可變可以保證執行緒安全。當多個執行緒同時呼叫同一個字串時,如果有一個執行緒改變了字串內容,那將是個很危險的操作
  3. 字串池的要求,字串常量池是一個特殊的儲存區。當字串符被建立,並且該字串已經存在池中,返回已有字串的引用,而不是建立新物件。如果String是可變的,通過一個字串引用改變字串,那麼會導致其他字串引用的值錯誤

字串共享

字串常量池String Pool是JVM例項全域性共享的,JAVA會確保池中每個不同的字串只存在一個拷貝,不會存在相同字串出現兩份拷貝在池中。這樣的設計模式稱為“享元模式”,採用一個共享來避免大量擁有相同內容物件的開銷

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);

因為相同的字串都是引用字串常量池中的一個字串常量,所以以上輸出的是true

JVM怎麼判斷新建立的字串需不需要在Java Heap(堆)中建立 新物件呢?

先根據比較與String Pool中某一個是否相等,如果有,則使用其引用。反之則根據的字面量建立一個字串物件,再將這個字面量與字串物件引用關聯起來


AbstractStringBuilder

AbstractStringBuilder是對可變字元序列的概念描述,其提供了可變字元序列的基本協議約定。其內部也有用於儲存字串的字元陣列,與String不同,其不被final修飾,也就是AbstractStringBuilder的內部成員陣列是可變的。

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

    /**
     * The count is the number of characters used.
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

成員變數count用於記錄實際的字元個數

通過有參構造可以為value引用一個指定大小的陣列,當然這是給子類呼叫的

既然底層是個陣列,陣列又是順序儲存的,那對陣列做操作必然會出現大量的元素移動

方法

獲取長度

@Override
public int length() {
    return count;
}

public int capacity() {
    return value.length;
}

length()用於獲取陣列實際資料的個數

capacity()獲取陣列的容量。

如果實際資料的個數超過陣列的容量,則容量自動增大

設定長度

public void setLength(int newLength) {
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    ensureCapacityInternal(newLength);

    if (count < newLength) {
        Arrays.fill(value, count, newLength, '\0');
    }

    count = newLength;
}

設定陣列的容量。

自動擴容

查閱原始碼,在每次對value陣列做操作時都會呼叫ensureCapacityInternal()方法用於確保空間大小足夠。

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

底層是拷貝陣列,重新分配大小,大小由newCapacity()方法來決定

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

擴容是原陣列長度*2再+2。

如果*2再+2之後的容量夠大,那陣列容量就使用這個數值

如果*2再+2之後的容量還不夠大,先檢查數值是否比Integer.MAX_VALUE還大,成立則OutOfMemoryError();再和MAX_ARRAY_SIZE比較,如數值較大,則使用數值,反之使用MAX_ARRAY_SIZE

去除未使用的空間

陣列中除count-1外,其他的索引都由'\0'來佔用,這就產生資源浪費

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

trimToSize()方法重新拷貝一個以count為目標容量的陣列

獲取和設定指定索引的值

@Override
public char charAt(int index) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    return value[index];
}

先檢查索引是否越界,再返回指定值

public void setCharAt(int index, char ch) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    value[index] = ch;
}

先檢查索引是否越界,再給索引處指定值

append方法

很多過載的append方法都會去呼叫getChars()方法實現從尾部插入數值

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
    if (srcBegin < 0)
        throw new StringIndexOutOfBoundsException(srcBegin);
    if ((srcEnd < 0) || (srcEnd > count))
        throw new StringIndexOutOfBoundsException(srcEnd);
    if (srcBegin > srcEnd)
        throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

StringBuileder、StringBuffer

和String不同的是,StringBuileder、StringBuffer是一個可變的字元序列。StringBuilder、StringBuffer也是實現CharSequence介面,但它倆還繼承了AbstractStringBuilder類。

它倆的內部方法基本都是從AbstractStringBuilder類繼承下來。建構函式也是呼叫自父類。

public StringBuffer() {
    super(16);
}
public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
}

三者的區別

1.String不可改變的,執行緒安全的;StringBuileder是可變的,非執行緒安全的;StringBuffer也是可變的,執行緒安全的,推薦在多執行緒裡使用。

String str = "hello";
str = str + " word";

上面程式碼在第一行我們建立了String物件,並把“hello”字串在常量池中的引用和str關聯。在執行第二行,先把“hello”和“word”做拼接,再把拼接後新的String物件儲存到常量池中,再把新的String物件在常量池中的引用和str關聯。而之前的物件並沒有發生變化,且之前的物件會被垃圾回收器CG回收掉。

而StringBuffer和StringBuilder則不會,因為底層沒有對陣列和類做final修飾,所以可以對這個陣列“重定義”,當對他們的字串做操作也就是對這個物件做操作,不會再去建立額外的物件

2.字串物件使用“+”與字串或字串物件做拼接時,編譯器碰到每個“+”時,會去new一個StringBuilder並呼叫append做拼接,最後再呼叫toString返回字串

String str = "hello";
str += " word";
str += "!!!";
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String hello
         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  word
        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 !!!
        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: return

從位元組碼可以看到,第3和23行新建了兩次StringBuilder,而這樣的拼接方式無疑是對記憶體的一種浪費,因為要額外建立物件,所以效率也不是很好。

如果在程式中要對字串物件做拼接,建議使用StringBuilder或StringBuiffer

2.在大多數情況下,執行速度上比較,StringBuilder > StringBuffer > String

但是,下面的程式碼就會是String執行的比較快

String str = "hello" + "word" + "!!!";
StringBuilder sb = new StringBuilder("hello").append("word").append("!!!");
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String helloword!!!
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: ldc           #4                  // String hello
         9: invokespecial #5                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        12: ldc           #6                  // String word
        14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: ldc           #8                  // String !!!
        19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: astore_2

從位元組碼可以看出,程式中的第一行程式碼,JVM會自動解析成String str = “helloword!!!“,因為這些字串都是編譯期間即可知的常量。這種情況,String會比StringBuffer執行的更快些,但是如果拼接的是物件而不是字串則不會這樣。

總結:如果只是簡單的的宣告字串,沒有過多的操作,那麼使用String或StringBuilder都可,但後續要對這個字串有過多頻繁的操作則建議使用StringBuilder。