1. 程式人生 > >String原始碼淺析

String原始碼淺析

如果問你,開發過程中用的最多的類是哪個?你可能回答是`HashMap`,一個原因就是HashMap的使用量的確很多,還有就是HashMap的內容在面試中經常被問起。 但是在開發過程中使用最多的類其實並不是HashMap類,而是“默默無聞”的String類。假如現在問你String類是怎麼實現的?這個類為什麼是不可變類?這個類為什麼不能被繼承?這些問題你都能回答麼。本文就從String原始碼出發,來看下String到底是怎麼實現的,並詳細介紹下String類的API的用法。 ## String原始碼結構 首先要說明的是本文的原始碼是以**JDK11為基準**,選擇JDK11的原因是JDK11是一個LTS版本(長期支援版本),沒選擇現階段還在廣泛使用的JDK8的原因是想在看原始碼的過程中學習下JDK的新特性。 還有要說下的就是:大家在看原始碼時一定要注意JDK的版本,因為不同版本的實現有較大的差異。比如說String的實現在高低版本中就差異比較大。如果你是一個部落格主,更加要註明程式碼的版本了,不然讀者可能會很疑惑,為什麼和自己之前看的不一樣。 好了,下面就言歸正傳來看下String在JDK11中的實現程式碼。 ```java public final class String implements Serializable, Comparable, CharSequence { @Stable //位元組陣列,存放String的內容,如果你看的是較低版本的原始碼,這個變數可能是char[]型別,這個其實是JDK9開始對String做的一個優化 //具體是做了什麼優化我們下面再講,這邊先賣個關子 private final byte[] value; //也是和String壓縮優化有關,指定當前的LATIN1碼還是UTF16碼 private final byte coder; //雜湊值 private int hash; //序列化Id private static final long serialVersionUID = -6849794470754667710L; //優化壓縮開關,預設開啟 static final boolean COMPACT_STRINGS = true; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; public static final Comparator CASE_INSENSITIVE_ORDER = new String.CaseInsensitiveComparator(); static final byte LATIN1 = 0; static final byte UTF16 = 1; //... 下面部分程式碼省略 } ``` 從實現的介面看,String類有如下特點: - String類被**final**關鍵字修飾,因此不能被繼承。 - String的成員變數value使用final修飾,因此是不可變的,執行緒安全; - String類實現了Serializable介面,可以實現序列化。 - String類實現了Comparable,可以比較大小。 - String類實現了CharSequence介面,String本質是個陣列,低版本中是char陣列,JDK9以後優化成byte陣列,從String的成員變數value就可以看出來。 這邊說一個看原始碼的小技巧:**看一個類的原始碼時,我們先看下這個類實現了哪些介面,就可以大概知道這個類的主要作用功能是什麼了。** ## JDK9對String的優化 這邊首先要講下`JDK 9`中對String的優化,如果你不瞭解這塊優化點的話,看String的程式碼時會感到非常疑惑。 **背景知識** 在Java中,一個位元組char佔用兩個位元組的記憶體空間。在低版本的JDK中,String的內部預設維護的是一個char[]陣列,也就是說一個字串中包含一個字元,這個字串內部就包含一個相應長度的字元陣列。這樣就會出現下面這種情況: ```java String s = "ddd"; String s1 = "自由之路"; ``` 上面兩個字串內部的情況實際上是: ```java char[] value = ['d','d','d']; char[] value1 = ['自','由','之','路']; ``` 對於字串s,我們發現其中每個字元其實都是可以用一個位元組表示的,而現在使用兩個字元的char型別來表示,明顯就浪費了一倍的記憶體空間。 而且根據統計,在實際程式執行中,字串中包含的字元大多都是可以用一個位元組表示的字元,所以優化的空間很大。優化的方式就是在String內部使用byte[]陣列來表示字串,而不是使用char[]陣列。當檢測到,字串中的所有字元在Unicode碼集中的碼值可以使用一個位元組表示時,就可以節省一半的空間。 ### JDK6 中的Compressed Strings 其實在JDK6中就對String類做過類似的優化:在Java 6引入了Compressed Strings,對於one byte per character的字串使用byte[],對於two bytes per character的字串繼續使用char[]。 使用-XX:+UseCompressedStrings來開啟上面的優化。不過由於開啟這個特性後會造成一些不可知的異常,這個特性在java7中被廢棄了,然後在java8被移除。 ### JDK9中的Compact String Java 9 重新採納字串壓縮這一概念。 和JDK6不同的是:**無論何時我們建立一個所有字元都能用一個位元組的 LATIN-1 編碼來描述的字串,都將在內部使用位元組陣列的形式儲存**,且每個字元都只佔用一個位元組。另一方面,如果字串中任一字元需要多於 8 位元位來表示時,該字串的所有字元都統統使用兩個位元組的 UTF-16 編碼來描述。因此基本上能如果可能,都將使用單位元組來表示一個字元。 ```java //佔用3個位元組 String ss = new String("ddd"); //佔用14個位元組 String s = "自由之路ddd"; ``` 現在的問題是:所有的字串操作如何執行? 怎樣才能區分字串是由 LATIN-1 還是 UTF-16 來編碼?為了處理這些問題,字串的內部實現進行了一些調整。引入了一個 `final` 修飾的成員變數 `coder`, 由它來儲存當前字串的編碼資訊。 ```java //所有的字串都用byte陣列儲存 private final byte[] value; //用coder標示字串中所有的字元是不是都可以用一個位元組表示,它的值只有兩個LATIN1:1,標示所有字元都可以用一個位元組表示,UTF16:標示字串中部分字元需要兩個位元組表示。 private final byte coder; //下面是兩個常量 static final byte LATIN1 = 0; static final byte UTF16 = 1; ``` 現在,大多數的字串操作都將檢查 `coder` 變數,從而採取特定的實現: ```Java public int indexOf(int ch, int fromIndex) { return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex) : StringUTF16.indexOf(value, ch, fromIndex); } private boolean isLatin1() { return COMPACT_STRINGS && coder == LATIN1; } ``` 我們再看下String的一個常用方法: ```java public int length() { return value.length >> coder; } ``` 這個方法是要計算字串的長度,含義也很清楚。根據coder欄位判斷當前的字串中一個字元使用幾個位元組表示,如果是coder等於0,也是LATIN1模式,那麼所有字元都是用一個位元組表示,直接返回byte[]陣列的長度就可以。 如果coder等於1,那麼標示字串中所有字元都是用兩個位元組表示的,計算字串的長度需要將byte[]陣列除以2。`value.length >> coder`就是這個意思。 因為對String做了上面的優化,所以String的很多方法在操作時都需要判斷現在的模式是LATIN1還是UTF16模式,具體的方法這邊就不一一舉例了。但是這些判斷對使用String的開發者時無感的。 當然,String的這個優化特性可以關閉,使用下面的啟動引數就可以。 ```bash +XX:-CompactStrings ``` ## String的常用構造方法 ```java //構建空字串 public String() { this.value = "".value; this.coder = "".coder; } //根據已有的字串,建立一個新的字串 @HotSpotIntrinsicCandidate public String(String original) { this.value = original.value; this.coder = original.coder; this.hash = original.hash; } //根據字元陣列,建立字串,建立的過程中有壓縮優化的邏輯,具體見下面的方法 public String(char[] value) { this((char[])value, 0, value.length, (Void)null); } String(char[] value, int off, int len, Void sig) { if (len == 0) { this.value = "".value; this.coder = "".coder; } else { if (COMPACT_STRINGS) { //如果發現這個字元陣列可以壓縮,就使用LATIN1方式 byte[] val = StringUTF16.compress(value, off, len); if (val != null) { this.value = val; this.coder = 0; return; } } //不能進行壓縮優化,還是使用UTF16的方式 this.coder = 1; this.value = StringUTF16.toBytes(value, off, len); } } ``` String中還有很多構造方法,但是都會大同小異,大家可以自己看原始碼。 ## String常用方法總結 這邊總結下String的常用方法,一些比較簡單的方法就不具體講了。我們挑選一些比較重要的方法,具體講下他們的使用方法。 - codePointAt(int index):返回下標是index的字元在Unicode碼集中的碼點值; - codePoints():返回字串中每個字元在Unicode碼集中的碼點值; - compareToIgnoreCase(String other):忽略大小寫比較字元大小; - concat(String other):字串拼接函式; - equalsIgnoreCase(String other):忽略大小寫比較字串; - format:字串格式化函式,比較有用; - getBytes(String charSet):獲取字串在特定編碼下的位元組陣列; - indexOf(String s):返回字串s的下標,不存在返回-1; - intren():作用是檢測常量池中是否有當前字串,有的話就返回常量池中的對像,沒有的話就將當前對像放入常量池。 - isBlank():如果字串為空或只包含空白字元,則返回true,否則返回false,JDK11新加的API; - length():返回字元長度; - lines():從字串返回按行分割的Stream,行分割福包括:n ,r 和rn,stream包含了按順序分割的行,行分隔符被移除了,這個方法會類似split(),但效能更好;這個也是JDK11新加的API - matchs(String regex):和某個正則是否匹配; - regionMatches(int firstStart, String other, int otherStart, int len):當某個字串呼叫該方法時,表示從當前字串的firstStart位置開始,取一個長度為len的子串;然後從另一個字串other的otherStart位置開始也取一個長度為len的子串,然後比較這兩個子串是否相同,如果這兩個子串相同則返回true,否則返回false。 - repeat():返回一個字串,其內容是字串重複n次後的結果,JDK11新加入的函式; - String[] split(String regex, int limit):分割字串,注意limit引數的使用,下面會詳細講; - startsWith(String prefix, int toffset):判斷字串是否以prefix打頭; - replace(char oldChar, char newChar):使用newChar替換所有的oldChar,不是基於正則表示式的; - replace(CharSequence target, CharSequence replacement):替換所有,基於正則表示式的; - replaceFirst(String regex, String replacement):替換regex匹配的第一個字串,基於正則表示式; - replaceAll(String regex, String replacement):替換regex匹配的所有字串,基於正則表示式; - strip() :去除字串前後的“全形和半形”空白字元,這個函式在JDK中11才引入,注意和trim的區別,關於全形和半形的區別,可以參考這篇[文章](https://www.cnblogs.com/Peter2014/p/12710531.html),還提供了stripLeading()和stripTrailing(),可以分別去掉頭部或尾部的空格; - subString(int fromIndex):從指定位置開始擷取到字串結尾部分的子串; - subString(int fromIndex,int endIndex):擷取字串指定下標的子串; - toCharArray():轉換成字元陣列; - toUpperCase(Locale locale) :小寫轉換成大寫; - toLowerCase(Locale locale):大寫轉換成小寫; - trim():去除字串前後的空白字元(空格、tab鍵、換行符等,具體的話是去除ascll碼小於32的字元),注意trim和strip的區別; - `valueof`系列方法:將其他型別的資料轉換成String型別,比如將bool、int和long等型別轉換成String型別。 ### concat字串拼接函式 concat函式是字串拼接函式,介紹這個函式並不是因為這個函式比較重要或者實現比較複雜。而是因為通過這個函式的原始碼我們可以看出很多String的特性。 ```java public String concat(String str) { //如果被拼接的字串的長度是0,直接返回自己 int olen = str.length(); if (olen == 0) { return this; } else { byte[] buf; //如果當前字串和被拼接的字串的編碼模式相同,都是LATIN1或者都是UTF16 if (this.coder() == str.coder()) { byte[] val = this.value; buf = str.value; //計算出新字串所需位元組的長度 int len = val.length + buf.length; byte[] buf = Arrays.copyOf(val, len); //使用系統函式拷貝 System.arraycopy(buf, 0, buf, val.length, buf.length); //根據新的位元組陣列生成一個新的字串 return new String(buf, this.coder); } else { //當前字串和被拼接的字串的編碼模式不同,那麼必須使用UTF16的編碼模式 int len = this.length(); buf = StringUTF16.newBytesFor(len + olen); this.getBytes(buf, 0, (byte)1); str.getBytes(buf, len, (byte)1); return new String(buf, (byte)1); } } } ``` ### format函式 String的format方法是一個很有用的方法,可以用來對字串、數字、日期和時間等進行格式化。 ```java //對整數格式化,4位顯示,不足4位補0 //超過4位,還是原樣顯示 int num = 999; String str = String.format("%04d", num); System.out.println(str); //對日期進行格式化 String format = String.format("%tF", new Date()); System.out.println(format); ``` format方法還有很多用法,大家可以自己查詢使用。 ### regionMatches 該方法的定義如下: ```java regionMatches(int firstStart, String other, int otherStart, int len) ``` 當某個字串呼叫該方法時,表示從當前字串的firstStart位置開始,取一個長度為len的子串;然後從另一個字串other的otherStart位置開始也取一個長度為len的子串,然後比較這兩個子串是否相同,如果這兩個子串相同則返回true,否則返回false。 該方法還有另一種過載: ```java str.regionMatches(boolean ignoreCase, int firstStart, String other, int otherStart, int len) ``` 可以看到只是多了一個boolean型別的引數,用來確定比較時是否忽略大小寫,當ignoreCase為true表示忽略大小寫。 ### split函式 String的split函式我們平時也經常使用,但是估計很多人都沒有注意這個函式的第二個引數:limit ```java public String[] split(String regex, int limit) ``` 首先,split方法的作用是根據給定的regex去分割字串,將分割完成的字元陣列返回。其中limit引數的作用是: - 當limit>0時,limit代表最後的陣列長度,同時一共會分割limit-1次,最後沒有切割完成的直接放在一起; - 當limit=0時(預設值),會盡量多去分割,並且如果分割完的字元陣列末尾是空字串,會去除這個空字串; - 當limit<0時,會盡量多去分割,但不會去掉末尾的空字串。 下面舉個列子: ```java String s1 = "部落格園|CSDN||"; String[] split1 = s1.split("\\|", 2); System.out.println("split1 length:" + split1.length); System.out.println("split1 content:" + Arrays.toString(split1)); String[] split2 = s1.split("\\|", 0); System.out.println("split2 length:" + split2.length); System.out.println("split2 content:" + Arrays.toString(split2)); String[] split3 = s1.split("\\|", -1); System.out.println("split3 length:" + split3.length); System.out.println("split3 content:" + Arrays.toString(split3)); System.out.println("---換一個複雜點的字串---"); s1 = "|部落格園||CSDN|自由之路ddd|"; split1 = s1.split("\\|", 2); System.out.println("split1 length:" + split1.length); System.out.println("split1 content:" + Arrays.toString(split1)); split2 = s1.split("\\|", 0); System.out.println("split2 length:" + split2.length); System.out.println("split2 content:" + Arrays.toString(split2)); split3 = s1.split("\\|", -1); System.out.println("split3 length:" + split3.length); System.out.println("split3 content:" + Arrays.toString(split3)); ``` 下面是輸出結果,對照著這個結果大家就應該能明白split方法的使用了 ```text split1 length:2 split1 content:[部落格園, CSDN|自由之路ddd|] split2 length:3 split2 content:[部落格園, CSDN, 自由之路ddd] split3 length:4 split3 content:[部落格園, CSDN, 自由之路ddd, ] ---換一個複雜點的字串--- split1 length:2 split1 content:[, 部落格園||CSDN|自由之路ddd|] split2 length:5 split2 content:[, 部落格園, , CSDN, 自由之路ddd] split3 length:6 split3 content:[, 部落格園, , CSDN, 自由之路ddd, ] ``` ## 總結 - String類被**final**關鍵字修飾,因此不能被繼承; - String的成員變數value使用final修飾,因此是不可變的,執行緒安全; - String中的方法對字串的操作都會生成一個新的String物件; - JDK9開始對String進行了優化,內部徹底使用byte[]陣列來代替char陣列。 ## 參考 - [Java 9 新特性 - Compact Strings](https://reionchan.github.io/2017/09/25/java-9-compact-string/) - [聊聊Java 9的Compact Strings](https://blog.csdn.net/weixin_34120274/article/details/91466378) - [Java11新增的String方法](https://www.jianshu.com/p/ab311687e3db) - [split第二個引數limit的用法](https://blog.csdn.net/wx1528159409/article/details/92796234) - [String之regionMatches方法](https://blog.csdn.net/ahence/article/details/27494457) - [String的格式化方法使用](https://www.cnblogs.com/fsjohnhuang/p/4094777.html#a7) ## 公眾號推薦 歡迎大家關注我的微信公眾號「程式設計師自由之路」 ![](https://img2020.cnblogs.com/blog/1775037/202005/1775037-20200505091245079-544605853.jpg)