JDK1.8原始碼閱讀——String
人不醜話也不多,直接開始啦~
一、結構預覽
由於String包含的方法是在太多,因此這裡就不列出它的結構樹了。給大家看一下本篇部落格的主要內容的目錄吧。
1. String類屬性
2. String成員變數
3. String構造器
4. String是如何重寫equals()的
5. String常用的工具方法
二、正文
1. String類屬性
首先看一下String類的定義:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
可以看到String繼承了序列化介面、Comparable介面以及CharSequence 介面
Serializable:序列化介面,至於為什麼要實現這個介面,個人覺得可能與資料的儲存與傳輸有關。由於String的應用範圍實在是太廣了,例如我們常用的資料庫連線資訊、http、socket埠和IP等都是字串,又比如redis、memcache等快取服務也使用了String作為快取型別,而這些應用場景,都與序列化密不可分。以上屬於個人理解,不當之處,可以指出。
Comparable:用於比較字串的大小,具體的實現方法在下文。
CharSequence:有序的字符集,CharSequence就是字元序列,String, StringBuilder和StringBuffer本質上都是通過字元陣列實現的,CharSequence 是 char 值的一個可讀序列。此介面對許多不同種類的 char 序列提供統一的只讀訪問。
2. String成員變數
private final char value[];
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
所以,一個字串是由一個序列化標識、雜湊值、儲存字串的char陣列 組成的。
注意:一個空字串的雜湊值為0,原因在下文介紹
3. String構造器
可以看到,構造一個String的方法是在是太多了,這裡就不做介紹了,主要說一下下面幾點:
3.1 String str = "" 與 new String()
這裡要提到java中的一個概念:常量池。那麼什麼是(String)常量池呢?
由於Java中String的應用是在是太廣泛了,可以說是隨處可見。為了避免資源的浪費,重複的去建立相同的字串,所以Java執行時會維護一個String Pool(String池), 也叫“字串緩衝區”。String池用來存放執行時中產生的各種字串,並且池中的字串的內容不重複。
使用String str = ""去構造一個字串時,會去常量池中去查詢池中是否存在這樣一個字串,如果存在,則直接飲用此字串,如果不存在,再去建立一個新的字串並維護到池中。
new String()則不進行判斷,直接建立一個新的字串。示例如下:
String str1 = "helloworld";
String str2 = new String("helloworld");
String str3 = "hello"+"world";
System.out.println(str1 == str2); //執行結果為:false
System.out.println(str2 == str3); //執行結果為:false
System.out.println(str1 == str3); //執行結果為:true
3.2 intern
public native String intern();
這裡順帶提一下intern這個方法,它的作用是返回當前String的記憶體地址或字串。當呼叫intern方法時,如果池中已經包含一個與該String
確定的字串相同equals(Object)
的字串,則返回該字串。否則,將此String
物件新增到池中,並返回此物件的引用。
String str1 = "helloworld";
String str2 = str1.intern();
System.out.println(str2); //列印helloworld
4 equals與hashCode
在上篇部落格中,我們知道如果重寫equals方法,那麼必須重寫hashCode方法。String對Object的equals進行了重寫,使其判斷的是字串內容是否相等,而不是引用地址。所以String的hashCode也是對字串的內容進行hash。
equals的原始碼如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
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,首先對其引用地址進行判斷,如果不同,再判斷其型別。在滿足型別相同的條件下,獲取其儲存字串的變數value,首先對長度進行判斷,這樣意味著“helloworld”與“hello world”是不相等的。如果長度相同,再通過迴圈去比較value陣列的每一個元素是否相同,只要存在一個元素不同,返回false。
上文說過,hashCode返回的是其字串內容的雜湊值,原始碼如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
注意這裡的判斷是如果hash為0或者字串長度>0,才會去計算hash值,否則返回預設的hash值0(int的預設值為0)。
關於這裡為何使用31,而不是定義其他的常量,網上大致的解釋有兩點,這裡僅做參考:
a. 31本身就是幾個優質的質因數之一,使用31能夠減少hash碰撞
b. 31=2<<5-1,效率故而較高
5. String常用的方法
5.1 isEmpty
public boolean isEmpty() {
return value.length == 0;
}
用於判斷字串是否為空,不包括null的情形,因此使用這個方法前需要去考慮它是否為null。
5.2 charAt
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
用於獲取指定位置的字元,使用該方法需要對字串長度進行判斷,否則有可能出現下標越界的異常。
5.3 compareTo
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
比較兩個字串的大小,本質上是比較阿斯克碼的大小,通常用於比較排序。需要注意的是,通過迴圈的內容,我們可以發現排序的優先順序是從前往後的,當靠前的字元不相等時,就會直接返回,不再繼續比較。
5.4 indexOf
public int indexOf(int ch) {
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
indexOf通常用於判斷某個字元是否存在於當前字串並獲取其下標,indexOf支援從0開始查詢,也支援從指定的下標位置開始查詢,如果不存在,返回-1。
5.5 substring
substring有多種引數的實現方法,這裡只介紹substring(int beginIndex),畢竟它們實現的方法基本相同。其原始碼如下:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
相對於indexOf,substring沒有處理beginIndex<0的情況,也沒有處理beginIndex超出字串長度的情況,都是直接丟擲了下標越界的異常,因此我們在使用時,一定要注意避免這種情況。
substring的實現方法為:String(char value[], int offset, int count),該方法是原始碼如下:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
在這個方法裡,主要關注的是Arrays提供的方法:public static char[] copyOfRange(char[] original, int from, int to),通過這裡我們可以知道,substring的實現是通過陣列的拷貝實現的,而陣列的拷貝底層是通過System.arraycopy這個non-java方法實現的。
5.6 split
我們常用的split(String regex)方法其底層是通過split(String regex, int limit)實現的。原始碼如下:
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
5.7 trim
去除字串頭尾的空格,中間部分的空格是不做處理的。原始碼如下:
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
其原理就是通過阿斯克碼值得判斷去擷取字串。
5.8 valueOf
獲取當前物件的String,大致包括以下方法:
valueOf(Object obj)
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
添加了null判斷,最終呼叫Object的toString方法。
valueOf(boolean b)
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
很簡單,就是一個三元表示式,返回true或false的字串。
除此之外,還包括一些基本資料型別的包裝類的valueOf方法,其底層都是呼叫對應的toString方法實現的,由於涉及到其他類的原始碼知識,這裡不做介紹。