1. 程式人生 > 其它 >String、Long原始碼解析

String、Long原始碼解析

技術標籤:java原始碼java

文章目錄


參考地址: 面試官系統精講Java原始碼及大廠真題

String

1.1 不變性

我們常常聽人說 String 是不可變類。這裡說的不可變指的是類值一旦被初始化,就不能再被改變了,如果被修改,將會是新的類,我們寫個 demo 來演示一下。

String s ="hello";
s ="world";

從程式碼上來看,s 的值好像被修改了,但從 debug 的日誌來看,其實是 s 的記憶體地址已經被修改了,也就說 s =“world” 這個看似簡單的賦值,其實已經把 s 的引用指向了新的 String,debug 的截圖顯示記憶體地址已經被修改,兩張截圖如下:
在這裡插入圖片描述
在這裡插入圖片描述我們從原始碼上檢視一下原因:

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

我們可以看出來兩點:

  • String 被 final 修飾,說明 String 類絕不可能被繼承了,也就是說任何對 String 的操作方法,都不會被繼承覆寫;
  • String 中儲存資料的是一個 char 的陣列 value。我們發現 value 也是被 final 修飾的,也就是說 value 一旦被賦值,記憶體地址是絕對無法修改的,而且 value 的許可權是 private 的,外部絕對訪問不到,String 也沒有開放出可以對 value 進行賦值的方法,所以說 value 一旦產生,記憶體地址就根本無法被修改。

因為 String 具有不變性,所以 String 的大多數操作方法,都會返回新的 String,如下面這種寫法是不對的:

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

1.2 字串亂碼

String str  ="nihao 你好 喬亂";
// 字串轉化成 byte 陣列
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 陣列轉化成字串
String s2 = new String(bytes);
log.info(s2);
// 結果列印為:
nihao ?? ??

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

1.3 首字母大小寫

我們經常要使類屬性的首字母小寫,這時候我們一般都會這麼做:
name.substring(0, 1).toLowerCase() + name.substring(1);,使用 substring 方法,該方法主要是為了擷取字串連續的一部分,substring 有兩個方法:

public String substring(int beginIndex, int endIndex) beginIndex:開始位置,endIndex:結束位置;

public String substring(int beginIndex)beginIndex:開始位置,結束位置為文字末尾。

substring 方法的底層使用的是字元陣列範圍擷取的方法 :Arrays.copyOfRange(字元陣列, 開始位置, 結束位置);從字元陣列中進行一段範圍的拷貝。

相反的,如果要修改成首字母大寫,只需要修改成 name.substring(0, 1).toUpperCase() + name.substring(1) 即可。

1.4 替換、刪除

替換在工作中也經常使用,有 replace 替換所有字元、replaceAll 批量替換字串、replaceFirst 替換遇到的第一個字串三種場景。

public void testReplace(){
  String str ="hello word !!";
  log.info("替換之前 :{}",str);
  str = str.replace('l','d');
  log.info("替換所有字元 :{}",str);
  str = str.replaceAll("d","l");
  log.info("替換全部 :{}",str);
  str = str.replaceFirst("l","");
  log.info("替換第一個 l :{}",str);
}
//輸出的結果是:
替換之前 :hello word !!
替換所有字元 :heddo word !!
替換全部 :hello worl !!
替換第一個 :helo worl !!

當然我們想要刪除某些字元,也可以使用 replace 方法,把想刪除的字元替換成 “” 即可。

1.5 拆分和合並

拆分我們使用 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"]

Long

1.1 快取

Long 最被我們關注的就是 Long 的快取問題,Long 自己實現了一種快取機制,快取了從 -128 到 127 內的所有 Long 值,如果是這個範圍內的 Long 值,就不會初始化,而是從快取中拿,快取初始化原始碼如下:

private static class LongCache {
    private LongCache(){}
    // 快取,範圍從 -128 到 127,+1 是因為有個 0
    static final Long cache[] = new Long[-(-128) + 127 + 1];

    // 容器初始化時,進行載入
    static {
        // 快取 Long 值,注意這裡是 i - 128 ,所以再拿的時候就需要 + 128
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

Integer 也是一樣

  public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
 Integer a = 128;
 Integer b = 128;  //底層呼叫的是valueOf方法
 System.out.println(a == b); //false   [-128,127]才會從快取裡拿  

總結

1 為什麼使用 Long 時,大家推薦多使用 valueOf 方法,少使用 parseLong 方法?

答:因為 Long 本身有快取機制,快取了 -128 到 127 範圍內的 Long,valueOf 方法會從快取中去拿值,如果命中快取,會減少資源的開銷,parseLong 方法就沒有這個機制。

2 如何解決 String 亂碼的問題?

答:亂碼的問題的根源主要是兩個:字符集不支援複雜漢字、二進位制進行轉化時字符集不匹配,所以在 String 亂碼時我們可以這麼做:

  • 所有可以指定字符集的地方強制指定字符集,比如 new String 和 getBytes 這兩個地方;
  • 我們應該使用 UTF-8 這種能完整支援複雜漢字的字符集。

3 為什麼都說 String 是不可變的

主要是因為 String 和儲存資料的 char 陣列,都被 final 關鍵字所修飾,所以是不可變的,具體細節描述可以參考上文。