jdk源碼分析(三)——String類

一.幾個(gè)概念

在我們正式開始看String源碼之前,先來了解幾個(gè)概念,對(duì)這幾個(gè)概念的理解,將有助于提升我們對(duì)代碼的認(rèn)識(shí)。
1.字面量
字面量是用于表達(dá)源代碼中一個(gè)固定值的表示法。數(shù)字,字符串等都有字面量表示。例如:

final int n = 1;
String s = "Hello World!"

上述代碼中1、"Hello World!"就是字面量。
2.常量池
(1)class文件中的常量池
在class文件中,除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯器生成的各種字面量和符號(hào)引用。
我們編寫如下代碼,并查看其class文件內(nèi)容:

public class Literals {
    final int n = 1;
    String s = "Hello World!";
}

在上圖中我們可以看到,字面量"1"、"Hello World!"出現(xiàn)在Constant pool列表中。
(2)運(yùn)行時(shí)常量池
根據(jù)《java虛擬機(jī)規(guī)范》的規(guī)定,java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:


class文件的常量池中的信息,將在類加載后進(jìn)入方法區(qū)中的常量池存儲(chǔ)。

3.字符集
字符集是一個(gè)系統(tǒng)支持的所有抽象字符的集合。常見的字符集有ascii字符集、Unicode字符集。
4.字符編碼
字符編碼是我們對(duì)字符集的一套編碼規(guī)則,將具體的字符進(jìn)行“數(shù)字化”,便于計(jì)算機(jī)理解和處理。例如我們常用的UTF-8字符編碼是對(duì)Unicode字符集的一種具體編碼規(guī)范。
5.碼位
我們已經(jīng)知道了字符集和字符編碼的概念,那么如何對(duì)具體的字符集進(jìn)行字符編碼呢?這就要用到碼位(code point)的概念:碼位是表示一個(gè)字符在碼空間中的數(shù)值。例如:ascii包含128個(gè)碼位(范圍是0-127),數(shù)字0的碼位是48。

二.核心代碼

1.類定義

public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence

需要注意的是,String類被聲明為final的,意味著它不可以被繼承。
另外,類實(shí)現(xiàn)了Serializable接口使它可以被序列化;實(shí)現(xiàn)了Comparable接口便于字符串之前的比較;實(shí)現(xiàn)了CharSequence接口,該接口是char值的一個(gè)可讀序列,它聲明了如下幾個(gè)方法:

public interface CharSequence {
    // 獲取字符序列長度
    int length();
    // 獲取某個(gè)指定位置的字符
    char charAt(int index);
    // 獲取子序列
    CharSequence subSequence(int start, int end);
    // 將字符序列轉(zhuǎn)換為字符串
    public String toString();
}

2.存儲(chǔ)機(jī)制
類的定義中實(shí)現(xiàn)了CharSequence接口,我們其實(shí)已經(jīng)大概可以了解,String是基于“字符序列”來實(shí)現(xiàn)的。通過看源代碼,我們可以確認(rèn):String是基于字符數(shù)組來進(jìn)行字符的存儲(chǔ)與管理的。代碼如下:

// 字符數(shù)組,用于存儲(chǔ)字符串中的字符
private final char value[];
// 字符串中第一個(gè)字符的下標(biāo)
private final int offset;
// 字符串中存儲(chǔ)的字符個(gè)數(shù)
private final int count;

以上代碼便構(gòu)成了String工作的基礎(chǔ):使用value數(shù)組來進(jìn)行字符存儲(chǔ),使用offset和count來進(jìn)行標(biāo)記和記錄?;舅械姆椒ǘ际菄@著這三個(gè)家伙展開的。

當(dāng)我們運(yùn)行如下代碼時(shí),程序?qū)嶋H上做了哪些事情呢?

String s = "Hello World!";

(1)在常量池中添加"Hello World!"字面量。
(2)在堆區(qū)創(chuàng)建一個(gè)String類型的對(duì)象實(shí)例。
(3)在棧區(qū)本地變量表中創(chuàng)建變量s,并指向堆區(qū)中的實(shí)例。
如下圖所示:

此外,為了節(jié)省空間,實(shí)際上String實(shí)例中的字符數(shù)組是可以被其他String實(shí)例復(fù)用的,這也就是offset變量和count變量存在的原因了,我們稍后再繼續(xù)討論這個(gè)問題。
3.常用方法
(1)構(gòu)造方法
我們常用的構(gòu)造方法有如下幾個(gè):

// 利用另一個(gè)字符串來生成一個(gè)新的字符串
String s1 = new String("Hello World!");
// 利用字節(jié)數(shù)組來生成字符串
String s2 = new String(s1.getBytes(), 0, s1.length(), "UTF-8");
char[] charArray = {'j', 'a', 'v', 'a'};
// 利用字符數(shù)組來生成字符串
String s3 = new String(charArray);

我們分別來看一下這三個(gè)構(gòu)造方法。

第一個(gè)構(gòu)造方法:
public String(String original) {
    // 獲取原字符串中的字符個(gè)數(shù)
    int size = original.count;
    // 獲取原字符數(shù)組
    char[] originalValue = original.value;
    char[] v;
    // 判斷原字符數(shù)組長度是否大于有效字符個(gè)數(shù),之所以需要判斷,是因?yàn)橛锌赡躱ffset不等于0
    // 即字符數(shù)組不是從第一個(gè)位置開始存儲(chǔ)的
    if (originalValue.length > size) {
        // 獲取原字符串中的首字符下標(biāo)
        int off = original.offset;
        // 對(duì)原數(shù)組進(jìn)行拷貝
        v = Arrays.copyOfRange(originalValue, off, off + size);
    } else {
        // 原字符數(shù)組長度等于字符個(gè)數(shù),也即offset=0
        v = originalValue;
    }
    // 以下是對(duì)構(gòu)成字符串的3個(gè)要素進(jìn)行賦值
    this.offset = 0;
    this.count = size;
    this.value = v;
}

第二個(gè)構(gòu)造方法:

public String(byte bytes[], int offset, int length, String charsetName)
        throws UnsupportedEncodingException {
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    checkBounds(bytes, offset, length);
    // 將字節(jié)數(shù)組反序列化為字符數(shù)組
    char[] v = StringCoding.decode(charsetName, bytes, offset, length);
    this.offset = 0;
    this.count = v.length;
    this.value = v;
}

// 邊界檢查,檢查傳入的字節(jié)數(shù)組,起始下標(biāo)、長度是否有效
// 這里有一個(gè)疑惑:為何該方法被聲明為static的?不知是何用意
// 因?yàn)檫@個(gè)方法只在構(gòu)造方法中被用到了,不是static也完全沒有問題
private static void checkBounds(byte[] bytes, int offset, int length) {
    if (length < 0)
        throw new StringIndexOutOfBoundsException(length);
    if (offset < 0)
        throw new StringIndexOutOfBoundsException(offset);
    if (offset > bytes.length - length)
        throw new StringIndexOutOfBoundsException(offset + length);
}

這里我們看到,代碼的核心邏輯在這一句:

char[] v = StringCoding.decode(charsetName, bytes, offset, length);

我們繼續(xù)看StringCoding.decode的實(shí)現(xiàn):

// 線程級(jí)緩存,緩存反序列化器
private static ThreadLocal decoder = new ThreadLocal();

static char[] decode(String charsetName, byte[] ba, int off, int len)
        throws UnsupportedEncodingException {
    // 從線程級(jí)緩存中獲取反序列化器
    StringDecoder sd = (StringDecoder) deref(decoder);
    // 如果charsetName為null,默認(rèn)使用ISO-8859-1字符編碼
    String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
    // 緩存中沒有反序列化器,或者雖然有,但是之前反序列化的字符集與這次不同,則重新生成decoder
    if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
            || csn.equals(sd.charsetName()))) {
        sd = null;
        try {
            Charset cs = lookupCharset(csn);
            if (cs != null)
                sd = new StringDecoder(cs, csn);
        } catch (IllegalCharsetNameException x) {
        }
        if (sd == null)
            throw new UnsupportedEncodingException(csn);
        // 將decoder放入線程級(jí)緩存,以備下次使用
        set(decoder, sd);
    }
    // 調(diào)用StringDecoder完成反序列化
    return sd.decode(ba, off, len);
}

// 從緩存中獲取反序列器,此處使用了軟引用,便于jvm在內(nèi)存不足時(shí),釋放該緩存
private static Object deref(ThreadLocal tl) {
    SoftReference sr = (SoftReference) tl.get();
    if (sr == null)
        return null;
    return sr.get();
}

// 判斷字符集是否支持,并加載字符集處理類
private static Charset lookupCharset(String csn) {
    if (Charset.isSupported(csn)) {
        try {
            return Charset.forName(csn);
        } catch (UnsupportedCharsetException x) {
            throw new Error(x);
        }
    }
    return null;
}

// 將對(duì)象的軟引用放入線程級(jí)緩存
private static void set(ThreadLocal tl, Object ob) {
    tl.set(new SoftReference(ob));
}

這段代碼較長,大體是利用了線程級(jí)緩存來緩存decoder,這樣就不必每次都實(shí)例化新的decoder,同時(shí)線程級(jí)緩存也確保了反序列化的操作是線程安全的。其中ThreadLocal和SoftReference結(jié)合的用法可以為我們所借鑒。
第三個(gè)構(gòu)造方法:

public String(char value[]) {
    this.offset = 0;
    this.count = value.length;
    this.value = StringValue.from(value);
}

StringValue.from(value)方法的具體實(shí)現(xiàn)如下:

static char[] from(char[] value) {
    return Arrays.copyOf(value, value.length);
} 

也就是進(jìn)行了數(shù)組拷貝,代碼比較簡(jiǎn)單,我們不再贅述。
(2)字符串比較方法

public int compareTo(String anotherString) {
    int len1 = count;
    int len2 = anotherString.count;
    // 獲取兩個(gè)字符串中長度較小者的長度
    int n = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
    int i = offset;
    int j = anotherString.offset;

    // 如果兩個(gè)字符串的offset相等
    if (i == j) {
        int k = i;
        int lim = n + i;
        // 逐個(gè)字符比較,如果相同位上的字符不同,則按照Unicode的大小進(jìn)行比較
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
    } else { // 兩個(gè)字符串的offset不相等
        // 逐個(gè)字符比較,如果相同位上的字符不同,則按照Unicode的大小進(jìn)行比較
        while (n-- != 0) {
            char c1 = v1[i++];
            char c2 = v2[j++];
            if (c1 != c2) {
                return c1 - c2;
            }
        }
    }
    // 如果仍然沒有比較出大小,說明前面n個(gè)字符都相等,則長度大的字符串更大
    return len1 - len2;
}

對(duì)于這個(gè)方法的實(shí)現(xiàn),我有些疑惑,原理上是對(duì)兩個(gè)字符串中的字符數(shù)組進(jìn)行逐個(gè)比較,這種比較方法即是”字典順序“比較。我的疑惑在于,為什么要判斷兩個(gè)字符串的offset是否相等呢?直接進(jìn)行else分支中的while循環(huán)不就可以了嗎?這一點(diǎn)暫時(shí)沒有想通。
我們常用的equals方法也是基于“字典順序”比較,主要邏輯與compareTo方法類似,此處就不再貼出代碼。
(3)hashCode方法

// 緩存字符串的hashCode,默認(rèn)為0
private int hash;

// 計(jì)算字符串的hashCode
public int hashCode() {
    int h = hash;
    int len = count;
    // 如果之前沒有計(jì)算過hashCode,且字符串長度不為0,則進(jìn)行計(jì)算
    if (h == 0 && len > 0) {
        int off = offset;
        char val[] = value;

        // 利用公式h=31*h + c計(jì)算hashCode,c為字符數(shù)組中每個(gè)字符的code point
        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        // 將計(jì)算好的hashCode緩存起來,以便下次使用
        hash = h;
    }
    return h;
}

我們?cè)?a href="http://www.itdecent.cn/p/4791207253a0" target="_blank">jdk源碼分析(一)中分析如何覆蓋hashCode方法時(shí),曾講到《effective java》中提到的一種方法,此處即是使用了這種方法來計(jì)算hashCode。
此外,在這段代碼中,值得注意的是:將整數(shù)值與char值相加會(huì)得到什么呢?根據(jù)java基本類型間的強(qiáng)制轉(zhuǎn)換規(guī)則,char型將會(huì)被轉(zhuǎn)換為int型,然后與int類型的值相加。那么char在轉(zhuǎn)換為int時(shí)該如何取值呢?其實(shí)這就利用了碼位(code point)的概念,我們可以通過程序來看一下。

String s = "abc123中國";
for (int i = 0; i < s.length(); i++) {
    System.out.println((int)s.charAt(i) + "," + s.codePointAt(i));
}

運(yùn)行程序,得到的結(jié)果如下:

97,97
98,98
99,99
49,49
50,50
51,51
20013,20013
22269,22269

由此可知,char字符轉(zhuǎn)換為整型時(shí),其值為其在Unicode字符集中的碼位。

(4)substring方法

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
            new String(offset + beginIndex, endIndex - beginIndex, value);
}

在經(jīng)過對(duì)參數(shù)的校驗(yàn)后,substring方法最終調(diào)用了一個(gè)有三個(gè)參數(shù)的構(gòu)造方法,我們來看一下:

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

我們剛才在講到String的存儲(chǔ)結(jié)構(gòu)時(shí)說,不同String實(shí)例是可以共用字符數(shù)組的,此處得到了印證:利用substring方法得到的子字符串和原字符串使用同一個(gè)字符數(shù)組value,只是offset和count不同而已。

String類中的方法還有很多,例如用于字符串連接的concat方法,字符串查找的indexOf方法,字符串替換的replace方法,以及獲取子字符串的substring方法等等,這些方法的原理不外乎圍繞著字符數(shù)組value、下標(biāo)offset、字符串長度count這幾個(gè)變量來展開,萬變不離其宗,此處不一一列舉。

三.相關(guān)類

除了String類之外,我們?nèi)粘>幋a時(shí)還經(jīng)常使用StringBuffer和StringBuilder,它們是對(duì)String的有益補(bǔ)充。由于String中的字符數(shù)組被聲明為final的,在賦值后就不允許被修改了,因此通常意義上,我們認(rèn)為String是”不可變“的。當(dāng)我們需要對(duì)字符串的值進(jìn)行頻繁修改時(shí),就可以使用StringBuffer和StringBuilder了。
我們來簡(jiǎn)單看一下這兩個(gè)類。

public final class StringBuffer
        extends AbstractStringBuilder
        implements java.io.Serializable, CharSequence
public final class StringBuilder
        extends AbstractStringBuilder
        implements java.io.Serializable, CharSequence

從定義中我們發(fā)現(xiàn),他們繼承自同一個(gè)父類AbstractStringBuilder,同時(shí)也實(shí)現(xiàn)了CharSequence接口,而String類也同樣實(shí)現(xiàn)了CharSequence接口,因此這三個(gè)類具有很多相同的方法,我們就拿length方法來比較一下。
StringBuilder類中,length方法(繼承自AbstractStringBuilder類)如下:

public int length() {
    return count;
}

而在StringBuffer類中,length方法如下:

public synchronized int length() {
    return count;
}

顯然,在StringBuffer中,方法的調(diào)用是同步的,在多線程環(huán)境中,一個(gè)線程需要等待另一個(gè)線程執(zhí)行完length方法后,才可以執(zhí)行,這也就是為什么我們常說StringBuffer是線程安全的原因。
大體來看,String、StringBuffer、StringBuilder三個(gè)類的差別主要如下:

線程安全 可變
String
StringBuffer
StringBuilder

此外,剛才說String在通常意義上我們認(rèn)為是”不可變“的,但是也并非絕對(duì),我們?nèi)匀豢梢岳梅瓷鋪砀淖僑tring的值,如下:

String java = "java";
System.out.println("old value:" + java);
try {
    Field field = java.getClass().getDeclaredField("value");
    field.setAccessible(true);
    char[] value = (char[]) field.get(java);
    value[0] = 'g';
    System.out.println("new value:" + java);
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

執(zhí)行程序,我們會(huì)得到如下結(jié)果:

old value:java
new value:gava
參考資料

1.《深入理解java虛擬機(jī)》
2.Java常量池理解與總結(jié)
3.初探Java字符串
4.Java常量池理解與總結(jié)
5.維基百科:碼位
6.維基百科:Unicode

本文已遷移至我的博客:http://ipenge.com/40983.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,637評(píng)論 18 399
  • Tip:筆者馬上畢業(yè)了,準(zhǔn)備開始 Java 的進(jìn)階學(xué)習(xí)計(jì)劃。于是打算先從 String 類的源碼分析入手,作為后面...
    石先閱讀 12,107評(píng)論 16 58
  • 寫著寫著發(fā)現(xiàn)簡(jiǎn)書提醒我文章接近字?jǐn)?shù)極限,建議我換一篇寫了。 建議52:推薦使用String直接量賦值 一般對(duì)象都是...
    我沒有三顆心臟閱讀 1,438評(píng)論 2 4
  • 今天寫這篇總結(jié),是因?yàn)閺奈揖毩?xí)瑜伽到現(xiàn)在,雖然在體式上稍有進(jìn)步,但是在心理技能上卻遇到了瓶頸,感覺不在狀態(tài)。于...
    卿云依依閱讀 979評(píng)論 0 0
  • 【組名】Sunshine 【組呼】別改天了,就今天吧,別以后了,就現(xiàn)在吧! 【組徽】 【組歌歌詞】 我相信我就是我...
    曉豬佩琪閱讀 330評(píng)論 1 1

友情鏈接更多精彩內(nèi)容