概述
String類無疑是日常開發中的使用最頻繁的類之一。其被final修飾:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
複製程式碼
因此可知String類是一個不可變類,且實現了Serializable,Comparable和CharSequence介面。
閾
hash
hash閾來用快取字串的hash code,預設為0。
/** Cache the hash code for the string */
private int hash; // Default to 0
複製程式碼
serialVersionUID
String類實現了Serializable介面,因此支援序列化和反序列化。serialVersionUID欄位則是用來在執行時判斷類版本的一致性。
value[]
String類用一個char陣列來儲存真實字元序列,就好比Integer中的欄位value表示Integer物件的實際值。並且這個陣列還被final修飾了,這正是我們所熟知的:String的內容一旦被初始化後,其值就不能再被修改了。當然,通過反射還是可以修改String內容的,不過沒啥意義。
/**The value is usesed for character storage.*/
private final char value[];
複製程式碼
建構函式
在jdk8中有14中不同的String類建構函式,下面記錄一些常用的。
public String()
很明顯,String類顯示的宣告瞭一個空建構函式。
public String() {
this.value = "".value;
}
複製程式碼
即使是空的建構函式,但還是會建立一個String物件,因此,下面這種字串的宣告方式,不僅雞肋而且浪費恐怖,不提倡:
String str = new String();
str = "realString";
複製程式碼
public Sting(String)
入參為一個字串的建構函式。其直接將入參字串的value域和hash域直接賦值給目標String。且String類是不可變類,因此不會對原有的入參字串產生副作用。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
複製程式碼
在實際開發中,我們應該直接採用字面量賦值的方式來初始化String物件(記為方法一),而不是通過上述這個建構函式new一個String物件(記為方法二)。這是因為:
- 方法一構造一個新的字串時,編譯器會優先從常量池中(堆中的一塊專門區域)查詢是否已經存在這個字串字面量。如果存在則直接返回該物件地址,否則構造一個新物件並儲存一份在常量池中。這實現了一種類似快取的機制,可以避免不必要的開銷。
- 方法二new一個String物件時,即使常量池在存在該物件的字面量,但還是會要求編譯器在堆中開闢一個新的記憶體空間來儲存新new出來的物件。
public static void main(String[] args){
String a = "string";
String b = "string";
String c = new String("string");
String d = new String("string");
System.out.println("a==b: " +(a == b));
System.out.println("a==c: " +(a == c));
System.out.println("c==d: " +(c == d));
}
---------------output------------
a==b: true // a和b都是常量池中同一個String物件
a==c: false // a是常量池中的物件,但c是堆記憶體中新申請的一塊空間
c==d: false // 雖然在常量池中存在字面量"string",但c和d都是堆記憶體中新建立的獨立物件
複製程式碼
public String(char[])
入參是一個char陣列,構造器呼叫Arrays.copyOf()方法將入參的字元序列依次複製成一個新的String物件,且不會對入參有副作用。
public String(char value[]) {
this.value = Arrays.copyof(value, vlaue.length);
}
複製程式碼
方法
equals(Object)方法
該方法重寫了Object的equals方法,用來判斷兩個字串是否包含相同的字元序列,最大時間複雜度為O(n)
,其中n字串長度。
public boolean equals(Object anObject) {
// 1. 引用同一物件,判定true
if (this == anObject) {
return true;
}
// 2. 入參不是String型別,判定false
if (anObject instancof String) {
String anotherString = (String) anObject;
int n = value.lenth;
// 3. 長度不匹配,判定false
if (n == anotherString.value.length) {
char v1[] = value; // 寫成char[] v1 = value,更規範點
char v2[] = anotherString.value;
int i = 0;
// 4. 逐字元比較,如不等,判定false
while (n-- != 0){
if (v1[i] != v2[i]) {
return false;
}
i++;
}
}
// 5. 否則,判斷true
return true;
}
return false;
}
複製程式碼
compareTo(String)方法
該方法實現了Comparable介面的compareTo方法,用來按字典順序比較兩個字串。該比較是基於字串中各個字元的Unicode值。
字典序
因為該方法比較兩個字串的大小,採用的是字典序,因此有必要了解一下字典序。
字典序定義:如果這兩個字串不同,那麼它們要麼在某個索引處的字元不同(該索引對二者均為有效索引),要麼長度不同,或者同時具備這兩種情況。如果它們在一個或多個索引位置上的字元不同,假設 k 是這類索引的最小值;則在位置k上具有較小值的那個字串(使用 < 運算子確定),其字典順序在其他字串之前。在這種情況下,compareTo
返回這兩個字串在位置k處兩個char值得差,即值:
this.charAt(k) - anotherString.charAt(k);
複製程式碼
如果沒有字元不同的索引位置,則較短字串的字典順序在較長字串之前。在這種情況下,compareTo返回這兩個字串長度的差,即值:
this.length() - anotherString.length();
複製程式碼
入參
anotherString: String,要比較的字串
返回值
- 正數:按字典序,呼叫者位於入參字串之後
- 負數:按字典序,呼叫者位於入參字串之前
- 0:兩者相等,此時若呼叫equals方法肯定是返回true
原始碼分析
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = min(len1, len2); // 取兩字串長度的最小值
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) { // 只遍歷min(len1, len2)次
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) { // 某個索引處字元不同,則返回字元的差值
return c1 - c2;
}
k++;
}
// 如果前個字元都相同,則返回長度差
return len1 - len2;
}
複製程式碼
這個方法寫的很巧妙。從初始索引開始,如果兩個物件能比較字元的地方都比較完了還相等,就直接返回長度差。此時如果兩個字串長度相等,則返回的剛好就是0,巧妙地判斷了返回值的三種情況。且是的最大時間複雜度為T=O(n)
,其中n為兩個字串的長度最小值。
可能有人好奇,為什麼沒有在方法體中看到對入參為null的防護?這是因為Comparable介面中已經定了,當入參為null時,compareTo方法會直接丟擲NullPointException
異常。
startsWith方法
startsWith(才發現原來中間還有個s,程式碼補全的鍋!)用來判斷字串是否以指定的字首開始。其包含兩種過載方法:
startsWith(String, int)
入參
- String prefix: 字首
- int toffset: 在此字串中開始查詢的位置
返回值
- true: 字元序列是此物件從索引 toffset 處開始的子字串字首
- false: toffset為負或大於此String物件的長度或字元序列不是此物件從索引 toffset 處開始的子字串字首
原始碼分析
public boolean startsWith(String prefix, int toffset) {
char ta[] = value; // 變數名為啥用ta,難道是target?
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
//如果起始地址小於0或者大於自身物件長度,返回false
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
// 從尾開始比較
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
複製程式碼
startsWith(String)
這個方法直接呼叫了startsWith(String, 0)
:
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
複製程式碼
endsWith(String)
相比startsWith,endsWith只有一種方法簽名。而且,endsWith的實現方式很巧妙,直接藉助了startsWith(String, int)
方法:
public boolean endsWith(String suffix) {
// 將判斷是否存在指定字尾轉換為去判斷是否是指定字首
return startsWith(suffix, value.length - suffix.value.length);
}
複製程式碼
replace方法
replace包含兩種過載形式,一種是替換字串中指定字元,另一種則是替換指定字元序列。
replace(char, char)
直接上原始碼,就是逐字元校驗,討巧之處在於先定位到舊值出現的位置,才去new一個char陣列儲存目標字串。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
// 找到oldChar最開始出現的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
// 從那個位置開始依次遍歷,用新值代替出現的舊值
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) { // 直接for迴圈拷貝
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
複製程式碼