String源碼與常見問題

String源碼與常見問題

String不變性(immutable)

不可變指的是類值一旦被初始化,就不能再被改變了,如果被修改,將會是新的對象。

String str = "hello";
str = "world";

image.pngimage.png
如上圖,是str在被賦值過程中,debug體現的變化。從代碼上來看,str 的值好像被修改了,但從 debug 的日誌來看,其實是 str 的內存地址已經被修改了,也就說 str =“world” 這個看似簡單的賦值,其實已經把 str 的引用指向了新的 String對象。

String 不可變的原因

從源碼出發,可以看到String 是被final修飾的;其value[] 也是被private final 修飾的(請關注value屬性,後文的許多分析案例,也會使用到)。也就是說任何對 String 類的操作方法,都不會被繼承覆寫;value[] 一旦被初始化,地址是無法被修改的,且外部也無法訪問此屬性。
以上兩點就是 String 不變性的原因,充分利用了 final 關鍵字的特性。如果我們自定義類時,希望也是不可變的,也可以模仿 String 的這兩點操作。

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

String 不可變帶來的操作影響

由於String 不可變,在許多String類的方法中,都會返回新的String對象。如下,是replace錯誤的使用方法

String str ="hello world !!";
// 這種寫法是替換不掉的,必須接受 replace 方法返回的參數纔行,這樣纔行:str = str.replace("l","dd");
str.replace("l","dd");

String 亂碼

進行二進制轉化操作時,經常在本地測試的都沒有問題,到其它環境機器上時,有時會出現字符串亂碼的情況。即一種"Work on my machine."問題。
這個主要是因爲在二進制轉化操作時,並沒有強制規定文件編碼,而不同的環境默認的文件編碼不一致導致的。

 String str = "nihao 你好 䳿";
// 字符串轉化成 byte 數組
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 數組轉化成字符串
String s2 = new String(bytes);
System.out.println(s2);
// 結果打印爲:
// nihao ?? ??

打印的結果爲??,這就是常見的亂碼錶現形式。這時候有同學說,是不是我把代碼修改成 String s2 = new String(bytes,"ISO-8859-1"); 就可以了?這是不行的。主要是因爲 ISO-8859-1 這種編碼對中文的支持有限,導致中文會顯示亂碼。唯一的解決辦法,就是在所有需要用到編碼的地方,都統一使用 UTF-8,對於 String 來說,getBytes 和 new String 兩個方法都會使用到編碼,我們把這兩處的編碼替換成 UTF-8 後,打印出的結果就正常了。

String 相等判斷

對象判斷相等時,會通過Object的equals方法進行判斷,每個繼承於Object的類,都可以重寫它。String的equals的源碼如下。

public boolean equals(Object anObject) {
    // 判斷內存地址是否相同
    if (this == anObject) {
        return true;
    }
    // 待比較的對象是否是 String,如果不是 String,直接返回 不相等
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // 兩個字符串的長度是否相等,不等則直接返回 不相等
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 依次比較每個字符是否相等,若有一個不等,直接返回 不相等
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

從 equals 的源碼可以看出,邏輯非常清晰,完全是根據 String 底層的結構(char 數組)來編寫出相等的代碼。這也提供了一種思路給我們:如果有人問如何判斷兩者是否相等時,我們可以從兩者的底層結構出發,去分析。

String 替換、刪除

替換在工作中也經常使用的場景

  1. replace 替換所有字符
  2. replaceAll 批量替換字符串
  3. replaceFirst 替換遇到的第一個字符串三種場景

其中在使用 replace 時需要注意,replace 有兩個方法,一個入參是 char,一個入參是 String,前者表示替換所有字符,如:name.replace('a','b'),後者表示替換所有字符串,如:name.replace("a","b"),兩者就是單引號和多引號的區別。
需要注意的是, replace 並不只是替換一個,是替換所有匹配到的字符或字符串哦。
當然我們想要刪除某些字符,也可以使用 replace 方法,把想刪除的字符替換成 “” 即可。

String 拆分和合並,建議使用Guava

拆分我們使用 split 方法,該方法有兩個入參數。第一個參數是我們拆分的標準字符,第二個參數是一個 int 值,叫 limit,來限制我們需要拆分成幾個元素。如果 limit 比實際能拆分的個數小,按照 limit 的個數進行拆分,我們演示一個 demo:

String s ="boo:and:foo";
// 我們對 s 進行了各種拆分,演示的代碼和結果是:
s.split(":") 結果:["boo","and","foo"]
s.split(":",2) 結果:["boo","and:foo"]
s.split(":",5) 結果:["boo","and","foo"]
s.split(":",-2) 結果:["boo","and","foo"]
s.split("o") 結果:["b","",":and:f"]
s.split("o",2) 結果:["b","o:and:foo"]

從演示的結果來看,limit 對拆分的結果,是具有限制作用的,還有就是拆分結果裏面不會出現被拆分的字段。那如果字符串裏面有一些空值呢,拆分的結果如下:

String a =",a,,b,";
a.split(",") 結果:["","a","","b"]

從拆分結果中,我們可以看到,空值是拆分不掉的,仍然成爲結果數組的一員,如果我們想刪除空值,只能自己拿到結果後再做操作,但 Guava(Google 開源的技術工具) 提供了一些可靠的工具類,可以幫助我們快速去掉空值,如下:

String a =",a, ,  b  c ,";
// Splitter 是 Guava 提供的 API 
List<String> list = Splitter.on(',')
    .trimResults()// 去掉空格
    .omitEmptyStrings()// 去掉空值
    .splitToList(a);
log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list));
// 打印出的結果爲:
["a","b  c"]

從打印的結果中,可以看到去掉了空格和空值,這正是我們工作中常常期望的結果,所以推薦使用 Guava 的 API 對字符串進行分割。
合併我們使用 join 方法,此方法是靜態的,我們可以直接使用。方法有兩個入參,參數一是合併的分隔符,參數二是合併的數據源,數據源支持數組和 List,在使用的時候,我們發現有兩個不太方便的地方:

  1. 不支持依次 join 多個字符串,比如我們想依次 join 字符串 s 和 s1,如果你這麼寫的話 String.join(",",s).join(",",s1) 最後得到的是 s1 的值,第一次 join 的值被第二次 join 覆蓋了;
  2. 如果 join 的是一個 List,無法自動過濾掉 null 值。

而 Guava 正好提供了 API,解決上述問題,我們來演示一下:

// 依次 join 多個字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
log.info("依次 join 多個字符串:{}",result);
List<String> list = Lists.newArrayList(new String[]{"hello","china",null});
log.info("自動刪除 list 中空值:{}",joiner.join(list));
// 輸出的結果爲;
依次 join 多個字符串:hello,china
自動刪除 list 中空值:hello,china

從結果中,我們可以看到 Guava 不僅僅支持多個字符串的合併,還幫助我們去掉了 List 中的空值,這就是我們在工作中常常需要得到的結果。

面試問題

String和其value[]爲什麼要用final修飾(爲什麼要設計爲不可變)?

String可以說是Java項目中使用頻率最高的類不爲過,綜合考慮到資源性能方面和安全角度等,使用final修飾。比如,在創建String時Jvm會先去常量池尋找已有的緩衝常量,如果String沒有被final修飾,這個時候被修改了其值,則可能會導致不可預料的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章