
一.幾個(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