天天看點

深入了解Java的String類String類字元串常量池字元串拼接總結

文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary

String類

在Java中String類的使用的頻率可謂相當高。它是Java語言中的核心類,在java.lang包下,主要用于字元串的比較、查找、拼接等等操作。如果要深入了解一個類,最好的方法就是看看源碼:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    //...
}           

從源碼中,可以看出以下幾點:

  • String類被final關鍵字修飾,表示String類不能被繼承,并且它的成員方法都預設為final方法。
  • String類實作了Serializable、CharSequence、 Comparable接口。
  • String類的值是通過char數組存儲的,并且char數組被private和final修飾,字元串一旦建立就不能再修改。

下面通過幾個問題不斷加深對String類的了解。

問題一

上面說字元串一旦建立就不能再修改,String類提供的replace()方法不就可以替換修改字元串的内容嗎?

實際上replace()方法并沒有對原字元串進行修改,而是建立了一個新的字元串傳回,看看源碼就知道了。

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            //建立一個新的字元串傳回
            return new String(buf, true);
        }
    }
    return this;
}           

其他方法也是一樣,無論是sub、concat還是replace操作都不是在原有的字元串上進行的,而是重新生成了一個新的字元串對象。

問題二

為什麼要使用final關鍵字修飾String類?

首先要講final修飾類的作用,被final修飾的類不能被繼承,類中的所有成員方法都會被隐式地指定為final方法。也就是不能擁有子類,成員方法也不能被重寫。

回到問題,String類被final修飾主要基于安全性和效率兩點考慮。

  • 安全性

因為字元串是不可變的,是以是多線程安全的,同一個字元串執行個體可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字元串自己便是線程安全的。

String被許多的Java類(庫)用來當做參數,比如網絡連接配接位址URL,檔案路徑path,還有反射機制所需要的String參數等,假若String不是固定不變的,将會引起各種安全隐患。

  • 效率

字元串不變性保證了hash碼的唯一性,是以可以放心的進行緩存,這也是一種性能優化手段,意味着不必每次都取計算新的哈希碼。

隻有當字元串是不可變的,字元串池才有可能實作,字元串常量池是java堆記憶體中一個特殊的存儲區域,當建立一個String對象,假如此字元串值已經存在于常量池中,則不會建立一個新的對象,而是引用已經存在的對象。

字元串常量池

字元串的配置設定和其他對象配置設定一樣,是需要消耗高昂的時間和空間的,而且字元串我們使用的非常多。JVM為了提高性能和減少記憶體的開銷,是以在執行個體化字元串的時候使用字元串常量池進行優化。

池化思想其實在Java中并不少見,字元串常量池也是類似的思想,當建立字元串時,JVM會首先檢查字元串常量池,如果該字元串已經存在常量池中,那麼就直接傳回常量池中的執行個體引用。如果字元串不存在常量池中,就會執行個體化該字元串并且将其放到常量池中。

我們可以寫個簡單的例子證明:

public static void main(String[] args) throws Exception {
    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2);//true
}           
深入了解Java的String類String類字元串常量池字元串拼接總結

還有一個面試中經常問的,new String(“abc”)建立了幾個對象?

這可能就是想考你對字元串常量池的了解,我一般回答是一個或者兩個對象。

如果之前"abc"字元串沒有使用過,毫無疑問是建立兩個對象,堆中建立了一個String對象,字元串常量池建立了一個,一共兩個。

如果之前已經使用過了"abc"字元串,則不會再在字元串常量池建立對象,而是從字元串常量緩沖區中擷取,隻會在堆中建立一個String對象。

String s1 = "abc";
String s2 = new String("abc");
//s2這行代碼,隻會建立一個對象           

字元串拼接

字元串的拼接在Java中是很常見的操作,但是拼接字元串并不是簡簡單單地使用"+"号即可,還有一些要注意的點,否則會造成效率低下。

比如下面這段代碼:

public static void main(String[] args) throws Exception {
    String s = "";
    for (int i = 0; i < 10; i++) {
        s+=i;
    }
    System.out.println(s);//0123456789
}           

在循環内使用+=拼接字元串會有什麼問題呢?我們反編譯一下看看就知道了。

深入了解Java的String類String類字元串常量池字元串拼接總結

其實反編譯後,我們可以看到String類使用"+="拼接的底層其實是使用StringBuilder,先初始化一個StringBuilder對象,然後使用append()方法拼接,最後使用toString()方法得到結果。

問題在于如果在循環體内使用+=拼接,會建立很多臨時的StringBuilder對象,拼接後再調用toString()賦給原String對象。這會生成大量臨時對象,嚴重影響性能。

是以在循環體内進行字元串拼接時,建議使用StringBuilder或者StringBuffer類,例子如下:

public static void main(String[] args) throws Exception {
    StringBuilder s = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        s.append(i);
    }
    System.out.println(s.toString());//0123456789
}           

StringBuilder和StringBuffer的差別在于,StringBuffer的方法都被sync關鍵字修飾,是以是線程安全的,而StringBuilder則是線程不安全的(效率高)。

總結

回顧一下,本文介紹了String類的不可變的特點,還有字元串常量池的作用,最後簡單地從JVM編譯的層面對字元串拼接提出一點建議。所謂溫故而知新,即使是一些很基礎很常見的類,如果深入去探索的話,也會有一番收獲。

這篇文章就講到這裡了,感謝大家的閱讀,希望看完大家能有所收獲!

我是一個努力讓大家記住的程式員。我們下期再見!!!

能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!