String類實現了java.io.Serializable, Comparable<String>, CharSequence這三個interface。
看了下這三個interface中的方法,發現CharSequence中在1.8版本jdk中新增了兩個方法:
1 public default IntStream chars(){...} 2 public default IntStream codePoints() {...}
注意這兩個方法是在interface內定義的,並且有方法實現的,interesting!而且還可以看到String並沒有實現這兩個方法。
這一系列的不可思議其實是Java 8 的新特性。default是Java 8 的新關鍵字。
摘抄一段網上的翻譯(未找到英文原文~~)
因為接口有這個語法限制,所以要直接改變/擴展接口內的方法變得非常困難。我們在嘗試強化Java 8 Collections API,讓其支持lambda表達式的時候,就面臨了這樣的挑戰。為了克服這個困難,Java 8中引入了一個新的概念,叫做default方法,也可以稱為Defender方法,或者虛擬擴展方法(Virtual extension methods)。 Default方法是指,在接口內部包含了一些默認的方法實現(也就是接口中可以包含方法體,這打破了Java之前版本對接口的語法限制),從而使得接口在進行擴展的時候,不會破壞與接口相關的實現類代碼。
來源:Java 8新特性——default方法(defender方法)介紹
有了default關鍵字,interface內部也可以定義方法了,於是便引入了一個C++ 中的菱形繼承問題(多繼承問題)。
1 interface A {} 2 interface B {}
3 class C implements A,B {}
如果A和B接口中都實現了一個同名的default方法,如果C類的對象不調用這個方法,則不會出問題,一旦調用了,則會出現Conflicting Exception異常,因為系統無法判斷該使用那個interface中的default方法。
回到CharSequence接口中定義的這兩個default方法,這兩個方法的定義都是為了支持Java 8 的stream新特性的。chars方法返回一個char的intstream,codepoint方法返回unicode codepoint的intstream。之后在研究下Java 8 stream特性再來理解此處。
String類擁有四個成員
1 private final char value[]; 2 private int hash; // Default to 0 3 private static final long serialVersionUID = -6849794470754667710L; 4 private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
從這四個成員不難看出String的本質其實是char[] 字符數組,需要注意的是這個字符數組是final的。final可以修飾類、方法和變量,含義各不相同。當修飾變量的時候意味着該變量一旦被初始化后將不能修改它的值,該變量只能創建和刪除,不能修改。
其次是一個int型的hash,表示的是該字符串的hash code。后面serialVersionUID用於序列化指定UID的。剩下的ObjectStreamField數組暫時不明白啥意思,官方解釋是Serializable類的Serializable字段的描述。 ObjectStreamFields的數組用於聲明一個類的Serializable字段。
接下來是String的構造函數。
默認構造函數構造出一個空的字符串,不是null。
其中有個構造函數如下:
1 public String(byte bytes[], int offset, int length, String charsetName)
這個構造函數是將byte數組按照charsetName進行解碼得到字符串,通常會用到UTF-8編碼,然而UTF-8編碼也有很多種寫法,比如:UTF-8, UTF8,UTF_8, utf-8, utf8, utf_8, uTf-8, Utf8等等,這些到底是否能被支持呢?寫了個例子發現UTF_8和utf_8不支持,其余都支持(如果使用Intellij idea則對於不支持的UTF_8這種格式會顯示為紅色),但是為啥能支持這么多亂七八糟的寫法呢?跟進去最關鍵的函數是Charset類中的lookup2函數:
1 private static Charset lookup(String charsetName) { 2 if (charsetName == null) 3 throw new IllegalArgumentException("Null charset name"); 4 Object[] a; 5 // 先從一級緩存中尋找 6 if ((a = cache1) != null && charsetName.equals(a[0])) 7 return (Charset)a[1]; 8 // We expect most programs to use one Charset repeatedly. 9 // We convey a hint to this effect to the VM by putting the 10 // level 1 cache miss code in a separate method. 11 return lookup2(charsetName); 12 } 13 14 private static Charset lookup2(String charsetName) { 15 Object[] a; 16 // 一級緩存未命中,再查找二級緩存 17 if ((a = cache2) != null && charsetName.equals(a[0])) { 18 cache2 = cache1; 19 cache1 = a; 20 return (Charset)a[1]; 21 } 22 // 仍然未命中,則去支持的字符集庫中去查找 23 Charset cs; 24 // private static CharsetProvider standardProvider = new StandardCharsets(); 25 // standardProvider其實是個StandardCharsets,StandardCharsets這個類里面定義了所有支持的標准字符集,並且包含其別名 26 // StandardCharsets繼承自FastCharsetProvider,因此最終是要調用FastCharsetProvider類的lookup函數,該函數第一句就是toLower(var1),然后再去StandardCharsets中定義的幾個hashmap中去尋找。 27 if ((cs = standardProvider.charsetForName(charsetName)) != null || 28 (cs = lookupExtendedCharset(charsetName)) != null || 29 (cs = lookupViaProviders(charsetName)) != null) 30 { 31 cache(charsetName, cs); 32 return cs; 33 } 34 35 /* Only need to check the name if we didn't find a charset for it */ 36 checkName(charsetName); 37 return null; 38 }
字符集默認的是ISO-8859-1,UTF-8兼容ISO-8859-1字符集。談到字符集又是一堆內容,本文略過。
1 public boolean equals(Object anObject)
equals函數是比較兩個字符串內容是否相同的一個函數,歸根到底比較的是char[],而"=="則比較的是兩個String的引用地址是否相同。
1 public boolean contentEquals(StringBuffer sb) 2 private boolean nonSyncContentEquals(AbstractStringBuilder sb) 3 public boolean contentEquals(CharSequence cs)
這三個函數中前兩個函數是用於比較當前字符串與給出的StringBuffer、StringBuilder的內容是否一致。因為StringBuffer是線程安全的,因此比較過程中也是加了同步synchronized, 而StringBuilder則是非線程安全的,比較過程就沒有同步鎖了。
1 public boolean equalsIgnoreCase(String anotherString)
equalsIgnoreCase也是很奇怪,字符比較過程中,先直接比較,再將兩個字符都轉為大寫進行比較,然后為了支持Georgian alphabet,再將兩個字符都轉為小寫進行比較,真是神奇啊。
1 public int hashCode() { 2 int h = hash; 3 if (h == 0 && value.length > 0) { 4 char val[] = value; 5 6 for (int i = 0; i < value.length; i++) { 7 h = 31 * h + val[i]; 8 } 9 hash = h; 10 } 11 return h; 12 }
此為hashCode計算方法:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],為什么用31作為乘數,可以去查看Why does Java's hashCode() in String use 31 as a multiplier? 其解釋如下
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically. (from Chapter 3, Item 9: Always override hashcode when you override equals, page 48)
1 public int indexOf(int ch, int fromIndex) 2 private int indexOfSupplementary(int ch, int fromIndex)
indexof用於在String中查找ch,實現過程中區分待查找的字符的code point是一個字節還是兩個字節,如果字符的code point 小於 0x010000則是一個字節的字符了,只需要比較單個字節就好;否則需要比較兩個字節。
補充code point概念(來源:代碼點(Code Point)和代碼單元(Code Unit)):
代碼點(Code Point):Unicode是屬於編碼字符集(CCS)的范圍。Unicode所做的事情就是將我們需要表示的字符表中的每個字符映射成一個數字,這個數字被稱為相應字符的碼點(code point)。例如“嚴”字在Unicode中對應的碼點是U+0x4E25。 代碼點是字符集被編碼后出現的概念。字符集(Code Set)是一個集合,集合中的元素就是字符,比如ASCII字符集,其中的字符就是'A'、'B'等字符。為了在計算機中處理字符集,必須把字符集數字化,就是給字符集中的每一個字符一個編號,計算機程序中要用字符,直接用這個編號就可以了。於是就出現了編碼后的字符集,叫做編碼字符集(Coded Code Set)。編碼字符集中每一個字符都和一個編號對應。那么這個編號就是代碼點(Code Point)。 碼元(Code Unit)是指一個已編碼的文本中具有最短的比特組合的單元。對於 UTF-8 來說,碼元是 8 比特長;對於 UTF-16 來說,碼元是 16 比特長。換一種說法就是 UTF-8 的是以一個字節為最小單位的,UTF-16 是以兩個字節為最小單位的。換一種說法就是UTF-8的是以一個字節為最小單位的,UTF-16是以兩個字節為最小單位的。 代碼單元是把代碼點存放到計算機后出現的概念。一個字符集,比如有10個字符,每一個字符從0到9依次編碼。那么代碼點就是0、1、。。。、9。為了在計算機中存儲這10個代代碼點,一個代碼點給一個字節,那么這里的一個字節就是一個代碼單元。比如Unicode是一個編碼字符集,其中有65536個字符,代碼點依次為0、1、2、。。。、65535,為了在計算機中表示這些代碼點就出現了代碼單元,65536個代碼點為了統一表示每個代碼點必須要有兩個字節表示才行。但是為了節省空間0-127的ASCII碼就可以不用兩個字節來表示,只需要一個字節,於是不同的表示方案就形成了不同的編碼方案,比如utf-8、utf-16等。對utf-8而言代碼單元就是一個字節,對utf-16而言代碼單元就是兩個字節。
1 public int lastIndexOf(int ch, int fromIndex) 2 private int lastIndexOfSupplementary(int ch, int fromIndex)
這兩個函數和上面的indexOf、indexOfSupplementary有點類似,一個是從前往后找,找到第一個返回;這兩個函數則是從后往前找,找到第一個返回。
1 int i = Math.min(fromIndex, value.length - 1)
這里面有個問題就是起始查找的位置是取fromIndex和value.length -1 (或者value.length - 2),這樣可以避免fromIndex越界問題。
1 static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
這個函數一開始就對fromIndex做了兩次容錯,考慮的比較全面。算法復雜度O(sourceCount * targetCount)
1 static int lastIndexOf(char[] source, int sourceOffset, int sourceCount, 2 char[] target, int targetOffset, int targetCount, 3 int fromIndex) { 4 // ...... 5 6 startSearchForLastChar: 7 while (true) { 8 while (i >= min && source[i] != strLastChar) { 9 i--; 10 } 11 if (i < min) { 12 return -1; 13 } 14 int j = i - 1; 15 int start = j - (targetCount - 1); 16 int k = strLastIndex - 1; 17 18 while (j > start) { 19 if (source[j--] != target[k--]) { 20 i--; 21 continue startSearchForLastChar; 22 } 23 } 24 return start - sourceOffset + 1; 25 } 26 }
在這個函數中使用到了label語法,continue label終止當前循環,繼續上層循環。lastIndexOf中查找也是從后往前查找的。
1 public String substring(int beginIndex) 2 public String substring(int beginIndex, int endIndex)
因為String的本質是final char[],因此substring中都是通過拷貝字符串中的字符創建出新的字符串的方式實現的
1 public String replace(char oldChar, char newChar) 2 public String replaceAll(String regex, String replacement)
replace是將字符串中所有的oldChar替換為newChar。replaceAll也具備這個功能,同時它還支持正則表達式。
1 public String[] split(String regex, int limit) { 2 /* fastpath if the regex is a 3 (1)one-char String and this character is not one of the 4 RegEx's meta characters ".$|()[{^?*+\\", or 5 (2)two-char String and the first char is the backslash and 6 the second is not the ascii digit or ascii letter. 7 */ 8 char ch = 0; 9 if (((regex.value.length == 1 && 10 ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || 11 (regex.length() == 2 && 12 regex.charAt(0) == '\\' && 13 (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && 14 ((ch-'a')|('z'-ch)) < 0 && 15 ((ch-'A')|('Z'-ch)) < 0)) && 16 (ch < Character.MIN_HIGH_SURROGATE || 17 ch > Character.MAX_LOW_SURROGATE)) 18 { 19 int off = 0; 20 int next = 0; 21 boolean limited = limit > 0; 22 ArrayList<String> list = new ArrayList<>(); 23 while ((next = indexOf(ch, off)) != -1) { 24 if (!limited || list.size() < limit - 1) { 25 list.add(substring(off, next)); 26 off = next + 1; 27 } else { // last one 28 //assert (list.size() == limit - 1); 29 list.add(substring(off, value.length)); 30 off = value.length; 31 break; 32 } 33 } 34 // If no match was found, return this 35 if (off == 0) 36 return new String[]{this}; 37 38 // Add remaining segment 39 if (!limited || list.size() < limit) 40 list.add(substring(off, value.length)); 41 42 // 當limit = 0的時候,結尾的空字符串將不會包含在返回數組中 43 // Construct result 44 int resultSize = list.size(); 45 if (limit == 0) { 46 while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { 47 resultSize--; 48 } 49 } 50 String[] result = new String[resultSize]; 51 return list.subList(0, resultSize).toArray(result); 52 } 53 return Pattern.compile(regex).split(this, limit); 54 }
這個split函數也是比較有意思,下面四行測試代碼返回結果是不同的,從代碼上來看,如果limit參數為0,則會將結尾的空字符串排除在返回的數組之外,因此",,a,b,c,d,,,,".split(",").length結果是6。而",,a,b,c,d,,,,".split(",", -1).length中因為limit參數為-1,則不會進行重整返回數組操作,結果就是我們通常理解的10了。如果limit > 0, 比如為5,則最終的返回數組長度必定是不大於5的,split次數為5-1=4次。
1 ",,a,b,c,d,,,,".split(",").length = 6 2 ",,a,b,c,d,,,,".split(",", -1).length = 10 3 ",,a,b,c,d,,,,".split(",", 5).length = 5 4 ",,a,b,c,d,,,,".split(",", 20).length = 10
1 public static String join(CharSequence delimiter, CharSequence... elements) 2 public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
1.8版本jdk增加了兩個join函數,用於將多個字符串按照分隔符delimiter進行重組。
1 public String trim() { 2 int len = value.length; 3 int st = 0; 4 char[] val = value; /* avoid getfield opcode */ 5 6 while ((st < len) && (val[st] <= ' ')) { 7 st++; 8 } 9 while ((st < len) && (val[len - 1] <= ' ')) { 10 len--; 11 } 12 return ((st > 0) || (len < value.length)) ? substring(st, len) : this; 13 }
trim用於將字符串開頭結尾的空白字符都去掉,注意在源碼中采用的是小於或等於' '字符的都去掉,查了下ASCII碼表,空格字符以下的字符包括\n \r \t \f \b \0等空白字符。
1 public native String intern();
最后還有個intern函數,這個函數是個native函數,測試用例:
1 String str1 = "a"; 2 String str2 = "b"; 3 String str3 = "ab"; 4 String str4 = "a" + "b"; 5 String str5 = str1 + str2; 6 String str6 = new String("ab"); 7 8 System.out.println(str4 == str3); // true 9 System.out.println(str5 == str3); // false 10 System.out.println(str6 == str3); // false 11 System.out.println(str4.intern() == str3); // true 12 System.out.println(str5.intern() == str3); // true 13 System.out.println(str6.intern() == str3); // true
String.intern()方法是一種手動將字符串加入常量池中的方法,當調用該方法時str.intern(),JVM就會在當前類的常量池中查找是否存在與str等值的String,若存在則直接返回常量池中相應Strnig的引用;若不存在,則會在常量池中創建一個等值的String,然后返回這個String在常量池中的引用(Java7, 8中會直接在常量池中保存當前字符串的引用)。因此,只要是等值的String對象,使用intern()方法返回的都是常量池中同一個String引用,所以,這些等值的String對象通過intern()后使用==是可以匹配的。(Java7中會直接在常量池中保存當前字符串的引用)。
另外要注意一點,str.intern()並不會改變str的地址,只會返回該字符串在常量池中的地址,如果不存在則jdk6拷貝一份放到常量池返回常量池中該字符串引用;jdk7和jdk8因為常量池就在堆內,因此是將該字符串的引用放入到常量池內。
再來看下面的兩段代碼:
1 public static void test3() { 2 String s3 = new String("1") + new String("1"); 3 String s4 = "11"; 4 s3.intern(); 5 System.out.println(s3 == s4); 6 System.out.println(s3.intern() == s4); 7 } 8 9 public static void test4() { 10 String s3 = new String("1") + new String("1"); 11 s3.intern(); 12 String s4 = "11"; 13 System.out.println(s3 == s4); 14 System.out.println(s3.intern() == s4); 15 }
這兩段代碼輸出結果都是false、true。因為str.intern()並不會改變str的地址,因此s3==s4是不可能相等的。
上面一段代碼常量池中因為聲明了變量s4,導致"11"這個字符串被存儲到常量池中,s3.intern()返回的也是常量池中"11"這個字符串的引用。
下面一段代碼則不同,因為在常量池中還沒有"11"這個字符串的時候就調用了s3.intern(),對於jdk7和jdk8而言,此時會將"11"存入到常量池中,但是s3地址未變(存入常量池中的“11”和s3地址是不同的),之后聲明的變量s4是常量池"11"的引用,因此s4和s3不等。s3.intern()指向的同樣是常量池中“11”,所以s3.intern()==s4。
參考連接:
https://www.zybuluo.com/pastqing/note/55097
http://www.importnew.com/7302.html
http://www.codeceo.com/article/java-8-default-method.html
http://www.cnblogs.com/zhangzl419/archive/2013/05/21/3090601.html
http://www.jianshu.com/p/95f516cb75ef
https://tech.meituan.com/in_depth_understanding_string_intern.html
https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier/299748