Java中字符串的操作可謂是最常見的操作了,String這個類它封裝了有關字符串操作的大部分方法,從構建一個字符串對象到對字符串的各種操作都封裝在該類中,本篇我們通過閱讀String類的源碼來深入理解下這些字符串操作背后的原理。主要內容如下:
- 繁雜的構造器
- 屬性狀態的常用函數
- 獲取內部數值的常用函數
- 比較大小的相關函數
- 局部操作等常用函數
一、繁雜的構造器
在學會操作字符串之前,我們應先了解下構造一個字符串對象的方式有幾種。先看第一種構造器:
private final char value[];
public String() {
this.value = "".value;
}
String源碼中第一個私有域就是value這個字符數組,該數組被聲明為final表示一旦初始化就不能被改變。也就是說一個字符串對象實際上是由一個字符數組組成的,並且該數組一旦被初始化則不能更改。這也很好的解釋了String對象的一個特性:不可變性。一經賦值則不能改變。而我們第一種構造器就很簡單,該構造器會將當前的string對象賦值為空(非null)。
接下來的幾種構造器都很簡單,實際上都是操作了value這個數組,但都不是直接操作,因為它不可更改,所以一般都是復制到局部來實現的各種操作。
//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);
}
無論是第一種的傳入一個String類型,還是第二種的直接傳入char數組的方式,都是轉換為為當前將要創建的對象中value數組屬性賦值。至於第三種方法,對傳入的char數組有要求,它要求從該數組索引位置為offset開始的后count個字符組成新的數組作為參數傳入。該方法首先做了幾個極端的判斷並增設了對應的異常拋出,核心方法是Arrays.copyOfRange這個方法,它才是真正實現字符數組拷貝的方法。
該方法傳入三個參數,形參value,起始位置索引,終止位置索引。在該方法中主要做了兩件事情,第一,通過起始位置和終止位置得到新數組的長度,第二,調用本地函數完成數組拷貝。
System.arraycopy(original, from, copy, 0,Math.min(original.length - from, newLength));
雖然該方法是本地方法,但是我們大致可以猜出他是如何實現的,無非是通過while或者for循環遍歷前者賦值后者。我們看個例子:
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);
}
輸出結果:wal
可以看見這是一種[ a,b)形式,也就是說索引包括起始位置,但不包括終止位置,所以上例中只截取了索引為0,1,2並沒有包括3,這種形式的截取方式在String的其他函數中也是常見的。
以上介紹的構建String對象的方式中,基本都是屬於操作它內部的字符數組來實現的,下面的幾種構造器則是通過操作字節數組來實現對字符串對象的構建,當然這些操作會涉及到編碼的問題。下面我們看第一個有關字節數組的構造器:
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,然后調用checkBounds方法判斷offset、length是否小於0,以及offset+length是否大於bytes.length。然后調用一個核心的方法用於將字節數組按照指定的編碼方式解析成char數組,我們可以看看這個方法:
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方法獲取對本地解碼器類的一個引用,接着使用三目表達式獲取指定的編碼標准,如果未指定編碼標准則默認為 ISO-8859-1,然后緊接着的判斷主要是:如果未能從本地線程相關類中獲取到StringDecoder,或者與指定的編碼標准不符,則手動創建一個StringDecoder實例對象。最后調用一個decode方法完成譯碼的工作。相比於該方法,我們更常用以下這個方法來將一個字節數組轉換成char數組。
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
只指定一個字節數組和一個編碼標准即可,當然內部調用的還是我們上述的那個構造器。當然也可以不指定任何編碼標准,那么則會使用默認的編碼標准:UTF-8
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
當然還可以更簡潔:
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
但是一般用於轉換字節數組成字符串的構造器還是使用由字節數組和編碼標准組成的兩個參數的構造器。
以上為String類中大部分構造器的源代碼,有些源碼和底層操作系統等方面知識相關聯,理解不深,見諒。下面我們看看有關String類的其他一些有關操作。
二、屬性狀態的常用函數
該分類的幾個函數還是相對而言較為簡單的,主要有以下幾個函數:
//返回字符串的長度
public int length() {
return value.length;
}
//判斷字符串是否為空
public boolean isEmpty() {
return value.length == 0;
}
//獲取字符串中指定位置的單個字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
有關字符串屬性的函數大致就這么些,相對而言比較簡單,下面看看獲取內部數值的常用函數。
三、獲取內部數值的常用函數
此分類下的函數主要有兩大類,一個是返回的字符數組,一個是返回的字節數組。我們首先看返回字符數組的方法。
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);
}
該函數用於將當前String對象中value字符數組的起始索引位置srcBegin到終止索引位置srcEnd拷貝到目標數組dst中,其中dst數組的起始位置為dstBegin索引處。看個例子:
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]);
}
}
結果如下:
我們指定從str 的[0,5)共五個字符組成一個數組,從chs數組索引為1開始,一個個復制到chs里。有關獲取獲取字符數組的函數就這么一個,下面我們看看獲取字節數組的函數。
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
這個函數的核心方法,StringCoding.encode和上述的StringCoding.decode很相似,只不過一個提供編碼標准是為了解碼成字符串對象,而另一個則是提供編碼標准為了將字符串編碼成字節數組。有關getBytes還有一些重載,但這些重載基本每個都會調用我們上述列出的這個方法,只是他們省略了一些參數(使用他們的默認值)。
四、判等函數
在我們日常的項目中可能經常會遇到equls這個函數,那么這個函數是否又是和符號 == 具有相同的功能呢?下面我們看看判等函數:
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方法,首先判斷兩個對象是否指向同一內存位置,如果是則返回true,如果不是才判斷他們內部封裝的數組是否是相等的。
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;
}
首先是檢錯判斷,簡單判斷下傳入的參數是否小於0等,然后通過不斷讀取兩個字符數組的字符比較是否相等,如果相等則直接跳過余下代碼進入下次循環,否則分別將這兩個字符轉換為小寫和大寫兩種形式進行比較,如果相等,依然返回true。equals方法只能判斷兩者是否相等,但是對於誰大誰小則無能為力。 下面我們看看compare相關方法,它可以表兩者大小。
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;
}
該方法將根據字典順序,判斷出兩者大小,代碼比較簡單,不再贅述。忽略大小寫的按字典順序排類似,主要涉及以下方法:
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
這里的compare方法是CASE_INSENSITIVE_ORDER類的一個內部類。
為了不讓文章篇幅過長,本篇暫時結束,下篇將介紹最常見的一些有關字符串操作的函數源碼,總結的不好,望海涵!