?????Java中字符串的操作可謂是最常見的操作了表悬,String這個(gè)類它封裝了有關(guān)字符串操作的大部分方法,從構(gòu)建一個(gè)字符串對象到對字符串的各種操作都封裝在該類中溅固,本篇我們通過閱讀String類的源碼來深入理解下這些字符串操作背后的原理名秀。主要內(nèi)容如下:
- 繁雜的構(gòu)造器
- 屬性狀態(tài)的常用函數(shù)
- 獲取內(nèi)部數(shù)值的常用函數(shù)
- 比較大小的相關(guān)函數(shù)
- 局部操作等常用函數(shù)
一、繁雜的構(gòu)造器
?????在學(xué)會操作字符串之前温亲,我們應(yīng)先了解下構(gòu)造一個(gè)字符串對象的方式有幾種棚壁。先看第一種構(gòu)造器:
private final char value[];
public String() {
this.value = "".value;
}
String源碼中第一個(gè)私有域就是value這個(gè)字符數(shù)組,該數(shù)組被聲明為final表示一旦初始化就不能被改變栈虚。也就是說一個(gè)字符串對象實(shí)際上是由一個(gè)字符數(shù)組組成的袖外,并且該數(shù)組一旦被初始化則不能更改。這也很好的解釋了String對象的一個(gè)特性:不可變性魂务。一經(jīng)賦值則不能改變曼验。而我們第一種構(gòu)造器就很簡單,該構(gòu)造器會將當(dāng)前的string對象賦值為空(非null)粘姜。
接下來的幾種構(gòu)造器都很簡單鬓照,實(shí)際上都是操作了value這個(gè)數(shù)組,但都不是直接操作孤紧,因?yàn)樗豢筛牟蝰桑砸话愣际菑?fù)制到局部來實(shí)現(xiàn)的各種操作。
//1
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//2
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//3
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);
}
無論是第一種的傳入一個(gè)String類型号显,還是第二種的直接傳入char數(shù)組的方式臭猜,都是轉(zhuǎn)換為為當(dāng)前將要創(chuàng)建的對象中value數(shù)組屬性賦值。至于第三種方法押蚤,對傳入的char數(shù)組有要求蔑歌,它要求從該數(shù)組索引位置為offset開始的后count個(gè)字符組成新的數(shù)組作為參數(shù)傳入。該方法首先做了幾個(gè)極端的判斷并增設(shè)了對應(yīng)的異常拋出揽碘,核心方法是Arrays.copyOfRange這個(gè)方法次屠,它才是真正實(shí)現(xiàn)字符數(shù)組拷貝的方法。
該方法傳入三個(gè)參數(shù)钾菊,形參value帅矗,起始位置索引偎肃,終止位置索引煞烫。在該方法中主要做了兩件事情,第一累颂,通過起始位置和終止位置得到新數(shù)組的長度滞详,第二凛俱,調(diào)用本地函數(shù)完成數(shù)組拷貝。
System.arraycopy(original, from, copy, 0,Math.min(original.length - from, newLength));
雖然該方法是本地方法料饥,但是我們大致可以猜出他是如何實(shí)現(xiàn)的蒲犬,無非是通過while或者for循環(huán)遍歷前者賦值后者。我們看個(gè)例子:
public static void main(String[] args){
char[] chs = new char[]{'w','a','l','k','e','r'};
String s = new String(chs,0,3);
System.out.println(s);
}
輸出結(jié)果:wal
可以看見這是一種[ a,b)形式岸啡,也就是說索引包括起始位置原叮,但不包括終止位置,所以上例中只截取了索引為0巡蘸,1奋隶,2并沒有包括3,這種形式的截取方式在String的其他函數(shù)中也是常見的悦荒。
以上介紹的構(gòu)建String對象的方式中唯欣,基本都是屬于操作它內(nèi)部的字符數(shù)組來實(shí)現(xiàn)的,下面的幾種構(gòu)造器則是通過操作字節(jié)數(shù)組來實(shí)現(xiàn)對字符串對象的構(gòu)建搬味,當(dāng)然這些操作會涉及到編碼的問題境氢。下面我們看第一個(gè)有關(guān)字節(jié)數(shù)組的構(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);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
該方法首先保證charsetName不為null,然后調(diào)用checkBounds方法判斷offset碰纬、length是否小于0萍聊,以及offset+length是否大于bytes.length。然后調(diào)用一個(gè)核心的方法用于將字節(jié)數(shù)組按照指定的編碼方式解析成char數(shù)組嘀趟,我們可以看看這個(gè)方法:
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException
{
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
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);
set(decoder, sd);
}
return sd.decode(ba, off, len);
}
首先通過deref方法獲取對本地解碼器類的一個(gè)引用脐区,接著使用三目表達(dá)式獲取指定的編碼標(biāo)準(zhǔn),如果未指定編碼標(biāo)準(zhǔn)則默認(rèn)為 ISO-8859-1她按,然后緊接著的判斷主要是:如果未能從本地線程相關(guān)類中獲取到StringDecoder牛隅,或者與指定的編碼標(biāo)準(zhǔn)不符,則手動創(chuàng)建一個(gè)StringDecoder實(shí)例對象酌泰。最后調(diào)用一個(gè)decode方法完成譯碼的工作媒佣。相比于該方法,我們更常用以下這個(gè)方法來將一個(gè)字節(jié)數(shù)組轉(zhuǎn)換成char數(shù)組陵刹。
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
只指定一個(gè)字節(jié)數(shù)組和一個(gè)編碼標(biāo)準(zhǔn)即可默伍,當(dāng)然內(nèi)部調(diào)用的還是我們上述的那個(gè)構(gòu)造器。當(dāng)然也可以不指定任何編碼標(biāo)準(zhǔn)衰琐,那么則會使用默認(rèn)的編碼標(biāo)準(zhǔn):UTF-8
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
當(dāng)然還可以更簡潔:
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
但是一般用于轉(zhuǎn)換字節(jié)數(shù)組成字符串的構(gòu)造器還是使用由字節(jié)數(shù)組和編碼標(biāo)準(zhǔn)組成的兩個(gè)參數(shù)的構(gòu)造器也糊。
以上為String類中大部分構(gòu)造器的源代碼,有些源碼和底層操作系統(tǒng)等方面知識相關(guān)聯(lián)羡宙,理解不深狸剃,見諒。下面我們看看有關(guān)String類的其他一些有關(guān)操作狗热。
二钞馁、屬性狀態(tài)的常用函數(shù)
?????該分類的幾個(gè)函數(shù)還是相對而言較為簡單的虑省,主要有以下幾個(gè)函數(shù):
//返回字符串的長度
public int length() {
return value.length;
}
//判斷字符串是否為空
public boolean isEmpty() {
return value.length == 0;
}
//獲取字符串中指定位置的單個(gè)字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
有關(guān)字符串屬性的函數(shù)大致就這么些,相對而言比較簡單僧凰,下面看看獲取內(nèi)部數(shù)值的常用函數(shù)探颈。
三、獲取內(nèi)部數(shù)值的常用函數(shù)
?????此分類下的函數(shù)主要有兩大類训措,一個(gè)是返回的字符數(shù)組伪节,一個(gè)是返回的字節(jié)數(shù)組。我們首先看返回字符數(shù)組的方法绩鸣。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
該函數(shù)用于將當(dāng)前String對象中value字符數(shù)組的起始索引位置srcBegin到終止索引位置srcEnd拷貝到目標(biāo)數(shù)組dst中架馋,其中dst數(shù)組的起始位置為dstBegin索引處∪疲看個(gè)例子:
public static void main(String[] args){
String str = "hello-walker";
char[] chs = new char[6];
str.getChars(0,5,chs,1);
for(int a=0;a<chs.length;a++){
System.out.println(chs[a]);
}
}
結(jié)果如下:
我們指定從str 的[0叉寂,5)共五個(gè)字符組成一個(gè)數(shù)組,從chs數(shù)組索引為1開始总珠,一個(gè)個(gè)復(fù)制到chs里屏鳍。有關(guān)獲取獲取字符數(shù)組的函數(shù)就這么一個(gè),下面我們看看獲取字節(jié)數(shù)組的函數(shù)局服。
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
這個(gè)函數(shù)的核心方法钓瞭,StringCoding.encode和上述的StringCoding.decode很相似,只不過一個(gè)提供編碼標(biāo)準(zhǔn)是為了解碼成字符串對象淫奔,而另一個(gè)則是提供編碼標(biāo)準(zhǔn)為了將字符串編碼成字節(jié)數(shù)組山涡。有關(guān)getBytes還有一些重載,但這些重載基本每個(gè)都會調(diào)用我們上述列出的這個(gè)方法唆迁,只是他們省略了一些參數(shù)(使用他們的默認(rèn)值)鸭丛。
四、判等函數(shù)
?????在我們?nèi)粘5捻?xiàng)目中可能經(jīng)常會遇到equls這個(gè)函數(shù)唐责,那么這個(gè)函數(shù)是否又是和符號 == 具有相同的功能呢鳞溉?下面我們看看判等函數(shù):
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;
}
我們看到該方法中,第一個(gè)判斷就使用了符號 == 鼠哥,實(shí)際上等于符號判斷的是:兩個(gè)對象是否指向同一內(nèi)存空間地址(當(dāng)然如果他們是指向同一內(nèi)存的熟菲,他們內(nèi)部封裝的數(shù)值自然也是相等的)。 從上述代碼中我們可以看出朴恳,這個(gè)equals方法抄罕,首先判斷兩個(gè)對象是否指向同一內(nèi)存位置,如果是則返回true于颖,如果不是才判斷他們內(nèi)部封裝的數(shù)組是否是相等的呆贿。
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
該方法是忽略大小寫的判等方法,核心方法是regionMatches:
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
if (ignoreCase) {
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
首先是檢錯(cuò)判斷恍飘,簡單判斷下傳入的參數(shù)是否小于0等榨崩,然后通過不斷讀取兩個(gè)字符數(shù)組的字符比較是否相等,如果相等則直接跳過余下代碼進(jìn)入下次循環(huán)章母,否則分別將這兩個(gè)字符轉(zhuǎn)換為小寫和大寫兩種形式進(jìn)行比較母蛛,如果相等,依然返回true乳怎。equals方法只能判斷兩者是否相等彩郊,但是對于誰大誰小則無能為力。 下面我們看看compare相關(guān)方法蚪缀,它可以表兩者大小秫逝。
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;
}
該方法將根據(jù)字典順序,判斷出兩者大小询枚,代碼比較簡單违帆,不再贅述。忽略大小寫的按字典順序排類似金蜀,主要涉及以下方法:
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
這里的compare方法是CASE_INSENSITIVE_ORDER類的一個(gè)內(nèi)部類刷后。
為了不讓文章篇幅過長,本篇暫時(shí)結(jié)束渊抄,下篇將介紹最常見的一些有關(guān)字符串操作的函數(shù)源碼尝胆,總結(jié)的不好,望海涵护桦!