【筆記1-String、Long 解析】精講Java源碼及大廠真題

1 String

1.1 不變性

咱們經常聽人說,HashMap 的 key 建議使用不可變類,好比說 String 這種不可變類。這裏說的不可變指的是類值一旦被初始化,就不能再被改變了,若是被修改,將會是新的類,咱們寫個 demo 來演示一下。java

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

從代碼上來看,s 的值好像被修改了,但從 debug 的日誌來看,實際上是 s 的內存地址已經被修改了,也就說 s =「world」 這個看似簡單的賦值,其實已經把 s 的引用指向了新的 String,debug 的截圖顯示內存地址已經被修改,兩張截圖以下:面試

file

file

圖片描述圖片描述咱們從源碼上查看一下緣由:數組

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 一旦產生,內存地址就根本沒法被修改。app

以上兩點就是 String 不變性的緣由,充分利用了 final 關鍵字的特性,若是你自定義類時,但願也是不可變的,也能夠模仿 String 的這兩點操做。工具

由於 String 具備不變性,因此 String 的大多數操做方法,都會返回新的 String,以下面這種寫法是不對的:測試

String str ="hello world !!";

// 這種寫法是替換不掉的
str.replace("l","dd");

// 必須接受 replace 方法返回的參數才行
str = str.replace("l","dd");

1.2 字符串亂碼

在生活中,咱們常常碰到這樣的場景,進行二進制轉化操做時,本地測試的都沒有問題,到其它環境機器上時,有時會出現字符串亂碼的狀況,這個主要是由於在二進制轉化操做時,並無強制規定文件編碼,而不一樣的環境默認的文件編碼不一致致使的。this

咱們也寫了一個 demo 來模仿一下字符串亂碼:編碼

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 首字母大小寫

若是咱們的項目被 Spring 託管的話,有時候咱們會經過 applicationContext.getBean(className); 這種方式獲得 SpringBean,這時 className 必須是要知足首字母小寫的,除了該場景,在反射場景下面,咱們也常常要使類屬性的首字母小寫,這時候咱們通常都會這麼作:

name.substring(0, 1).toLowerCase() + name.substring(1);

使用 substring 方法,該方法主要是爲了截取字符串連續的一部分,substring 有兩個方法:

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

// beginIndex:開始位置,結束位置爲文本末尾。
public String substring(int beginIndex)

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

相反的,若是要修改爲首字母大寫,只須要修改爲 name.substring(0, 1).toUpperCase() + name.substring(1) 便可。

1.4 相等判斷

咱們判斷相等有兩種辦法,equals 和 equalsIgnoreCase。後者判斷相等時,會忽略大小寫,近期看見一些面試題在問:若是讓你寫判斷兩個 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 底層的結構來編寫出相等的代碼。這也提供了一種思路給咱們:若是有人問如何判斷二者是否相等時,咱們能夠從二者的底層結構出發,這樣能夠迅速想到一種貼合實際的思路和方法,就像 String 底層的數據結構是 char 的數組同樣,判斷相等時,就挨個比較 char 數組中的字符是否相等便可。

1.5 替換、刪除

替換在工做中也常用,有 replace 替換全部字符、replaceAll 批量替換字符串、replaceFirst 替換遇到的第一個字符串三種場景。

其中在使用 replace 時須要注意,replace 有兩個方法,一個入參是 char,一個入參是 String,前者表示替換全部字符,如:name.replace('a','b'),後者表示替換全部字符串,如:name.replace("a","b"),二者就是單引號和多引號的區別。

須要注意的是, replace 並不僅是替換一個,是替換全部匹配到的字符或字符串哦。

寫了一個 demo 演示一下三種場景:

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.6 拆分和合並

拆分咱們使用 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,在使用的時候,咱們發現有兩個不太方便的地方:

  • 不支持依次 join 多個字符串,好比咱們想依次 join 字符串 s 和 s1,若是你這麼寫的話 String.join(",",s).join(",",s1) 最後獲得的是 s1 的值,第一次 join 的值被第二次 join 覆蓋了;
  • 若是 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 中的空值,這就是咱們在工做中經常須要獲得的結果。

2 Long

2.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);
    }
}

3 面試題

3.1 爲何使用 Long 時,你們推薦多使用 valueOf 方法,少使用 parseLong 方法

答:由於 Long 自己有緩存機制,緩存了 -128 到 127 範圍內的 Long,valueOf 方法會從緩存中去拿值,若是命中緩存,會減小資源的開銷,parseLong 方法就沒有這個機制。

3.2 如何解決 String 亂碼的問題

答:亂碼的問題的根源主要是兩個:字符集不支持複雜漢字、二進制進行轉化時字符集不匹配,因此在 String 亂碼時咱們能夠這麼作:
全部能夠指定字符集的地方強制指定字符集,好比 new String 和 getBytes 這兩個地方;
咱們應該使用 UTF-8 這種能完整支持複雜漢字的字符集。

3.3 爲何你們都說 String 是不可變的

答:主要是由於 String 和保存數據的 char 數組,都被 final 關鍵字所修飾,因此是不可變的,具體細節描述能夠參考上文。

相關文章
相關標籤/搜索