查看所有目錄
String類是我們日常開發中使用最頻繁的類之一,曾今有人說String類用的好壞能評判你是否是一個合格的java程序員。
基礎知識:
String對象的存放位置:大家都知道java中的對象大都是存放在堆中的,但是String對象是一個特例,它被存放在常量池中。
當創建一個字面量String對象時,首先會去檢查常量池中這個對象的存在與否。
java本地方法:一個本地方法就是一個java調用非java代碼的接口。該方法是非java實現,由C或C++語言實現。形式是:
修飾符 native 返回值類型 本地方法名(); 如public native String intern();
在我們看java源碼時如果追溯到了本地方法,在java層面上就到頭了,如果需要更深層次的了解本地方法的實現,就需要下載openjdk源碼然后看它是如何實現的了。有興趣的同學可以看如何查看java本地方法這篇文章。
String類:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
可以看到String類是final修飾的不能被繼承,同時它實現了Serializable接口可以序列化和反序列化,實現了Comparable支持字符串的比較,實現了CharSequence接口說明它是一個字符序列。
成員變量:
private final char value[];//存儲字符串
private int hash; //字符串的hash code 默認是0
private static final long serialVersionUID = -6849794470754667710L;//序列化id
String對象的字符串實際是維護在一個字符數組中的。操作字符串實際上就是操作這個字符數組,而且這個數組也是final修飾的不能夠被改變。
構造方法:
public String() {
this.value = "".value;
}
無參構造方法,值為空串。基本不用。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
參數String對象參數來構造String對象,該構造函數經常被用來做面試題。問new String("abc");共創建了幾個對象。答案是兩個,字面量"abc"創建一個對象放在常量池中,new String()又創建一個對象放在堆中。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
通過整個char數組參數來構造String對象,實際將參數char數組值復制給String對象的char數組。
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對象,具體哪一部分由offset和count決定,其中做了些參數檢查,傳入非法參數會報數組越界異常StringIndexOutOfBoundsException
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);
}
通過byte數組構造String對象,將入參byte數組中指定內容,用指定charsetName的字符集轉換后構造String對象。
其中StringCoding.decode(charsetName, bytes, offset, length)是根據指定編碼對byte數組進行解碼,解碼返回char數組。
checkBounds(bytes, offset, length)是對參數進行檢查(源碼如下),該方法是私有的只能在String類中調用。
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);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
該構造方法與上述構造方法類似,只不過這里的字符集是用Charset指定的,上述是用String指定的。Charset與charsetName是java中表示字符集的兩種不同形式。它們之間相互轉換如下:
字符串轉Charset對象:Charset charset = Charset.forName("UTF-8");
Charset對象轉字符串:String s = charset.displayName();
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
這兩個方法同上述兩個方法類似,上述是轉換byte數組中的部分數據構造String對象,這里是轉換全部byte數組構造String對象。通過轉換byte數組構造String對象在工作中還是挺常用的。
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);
}
通過byte數組,不指定字符集構造String對象。實際要在 StringCoding.decode(bytes, offset, length);解碼byte數組的時候會構造默認的字符編碼,默認的也就是系統默認的可能過GBK,可能過UTF-8,也可能是其它??赏ㄟ^-Dfile.encoding=UTF-8進行修改系統默認編碼。
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
通過StringBuffer構造String對象,StringBuffer內部也是維護了一個char數組,這里將StringBuffer數組中的內容復制給String對象中的數組。而且StringBuffer是線程安全的,所以這里也加了synchronized塊保證線程安全。
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
通過StringBuilder構造String對象,原理同StringBuffer一樣,只不過StringBuilder是線程不安全的,所在這里沒有加synchronized塊?;A面試中面試官經常詢問StringBuffer與StringBuilder的區別,有興趣的同學可以搜一下。
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
乍一看不知道這個構造函數是用來干嘛的,仔細分析就知道這個函數大有作用。首先它同String(char[] value)函數相比多了個參數share,雖然在方法本身沒有用到share,目前是只支持true,注釋也說了不支持false。這個方法定義成這樣應該是為了同String(char[] value)進行區分。否則沒辦法構成方法重載。再來看下這個方法的作用。它是直接將參數的地址傳給了String對象,這樣要比直接使用String(char[] value)的效率要高,因為String(char[] value)是逐一拷貝。有人會問這樣Stirng對象和參數傳過來的char[] value共享同一個數組,不就破壞了字符串的不可變性。設計都也考慮到了,所以它設置了保護用protected修飾而沒有公開出去。所以從安全性角度考慮,他也是安全的。在java中也有很多地方用到了這種性能好的、節約內存的、安全的構造函數。如replace、concat、valueOf等方法。
其它方法
length:
public int length() {
return value.length;
}
獲取字符串的長度,實際上就是返回內部數組的長度。因為char數組被final修飾是不可變的,只要構造完成char數組中的內容長度都不會改變,所以這里可以直接返回數組的長度。
isEmpty:
public boolean isEmpty() {
return value.length == 0;
}
判斷字符串是否為空。同理如果char的長度為0則表示字符串空。
charAt:
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
獲取字符串中指定索引位置的字符。先判斷索引是否合法,不能小于0或是大于字符串長度。然后直接返回數組對應位置的字符。
codePointAt:
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
同charAt類似,獲取字符串指定索引位置的字符的代碼點。也就是將對應位置的字符轉換成UniCode。
codePointBefore:
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}
獲取指定索引位置前一個位置的字符的代碼點。
codePointCount:
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
}
獲取字符串代碼點個數,是實際上的字符個數。length()方法返回的是使用的是UTF-16編碼的字符代碼單元數量,不一定是實際上我們認為的字符個數。如 String str = “/uD835/uDD6B”,那么機器會識別它是2個代碼單元代理的1個代碼點"Z",故而,length的結果是代碼單元數量2,而codePointCount()的結果是代碼點數量1。
getChars(char dst[], int dstBegin):
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
復制字符串中數組內容到指定字符數組指定位置中。該方法并沒有范圍檢查,方法僅供內部使用不對外公開。
getChars(int srcBegin, int srcEnd, char dst[], int dstBegin):
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);
}
獲取字符串中指定位置的字符到目標字符數組中。該方法為公有的,做了范圍檢查??梢钥吹綄ν馓峁┑姆椒ㄟ€是要嚴謹一些。在工作中也是一樣,內部使用的可以稍微寬松一些,對外提供的需要做嚴格的限制。
getBytes:
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
獲取字符串對應的字節數組。將字符串編碼成byte數組并返回,其中前兩個方法是調用者指定字符集,后一個方法使用系統默認的字符集。
equals:
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;
}
重寫了Object類的equals方法,這里是比較兩個字符器的內容是否完全相等。先判斷長度是否相等,長度不相等字符串必然不相等。然后再逐一比較每個對應位置的字符是否相等,如果全部相等則返回true,否則返回false。
contentEquals(StringBuffer sb) :
public boolean contentEquals(StringBuffer sb) {
return SequencontentEquals((Charce)sb);
}
比較字符串與StringBuffer對象的內容是否相等,調用了contentEquals(CharSequence cs) 方法,該方法實現如下。
contentEquals(CharSequence cs) :
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
比較字符串內容與字符序列內容是否相等。首先判斷是否為AbstractStringBuilder類型,AbstractStringBuilder有兩種實現方式,StringBuffer(線程安全的)和StringBuilder(線程不安全的),如果是則判斷是否為StringBuffer類型,此類判斷需要加鎖以保證線程安全,它們兩個都調用了nonSyncContentEquals方法進行判斷(見下)。其次判斷是否為String類型,因為String類也實現了CharSequence接口,如果是則調用String類的equals方法。最后如果是其它字符序列,則逐一比較字符數組中每個位置的字符是否相等。
nonSyncContentEquals:
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
char v1[] = value;
char v2[] = sb.getValue();
int n = v1.length;
if (n != sb.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
return true;
}
該方法為私有的供String類內部使用,也使用了相同的邏輯,先判字符數組長度是否相等,再逐一進行比較。其實不論是String的equals方法,contentEquals(CharSequence cs) 方法還是nonSyncContentEquals方法里面的比較邏輯都差不多,是否可以考慮將這類似的邏輯抽取出來單獨成立個方法呢。
equalsIgnoreCase:
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
從方法名也可以看出,該方法是忽略大小寫比較字符串內容是否相同。先判斷兩個對象地址是否一樣,地址一樣內容自然也一樣。再判斷長度,如果長度一樣再調用regionMatches方法進行比較(見后)。這里用了&&邏輯運算符的斷路原理,如果前一個判斷為假,后面的判斷就沒意義了。
compareTo:
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;
}
該方法是實現Comparable接口的方法,用于對字符串進行比較大小。邏輯是對兩個字符串中的數組進行逐位比較大小,從第一位開始比較,大的字符串就大,如果相同就繼續向下比較,直到比較出大小為止。這里取了兩個字符串中長度較小的作為循環次數。從源碼也可以看出字符串比較并不是我們表面上認為的先進行長度比較,長度不一樣再進行每個位置的比較。
內部內
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
該內部內實際上就是String類定義的一個內部比較器,私有的僅供內部使用,用于進行忽略大小寫比較字符串是否相等。CaseInsensitiveComparator(大小寫不敏感比較器),只看名字不看其具體實現也能大致看出來其作用,可見起一個好的名字是多么的重要。比較邏輯是對每位字符逐一進行比較,如果不等則將字符轉換為對應的大寫字符再進行比較,如果還不等再轉換為對應的小寫進行比較,最后返回兩個字符的大小即為整個字符串的大小。這里先轉換為大寫,再轉換為小寫的目的是不是所有的字符都是用英文字母進行表示的,比如漢字等。
compareToIgnoreCase:
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
忽略大小寫比較兩個字符串的大小,用到了上述內部內的比較邏輯。
regionMatches(int toffset, String other, int ooffset,int len):
public boolean regionMatches(int toffset, String other, int ooffset,
int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
區域比較,比較兩個字符串指定區域指定長度的內容是否相等。從指定區域,開始逐一比較指定長度字符數組內容是否相等。
regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len) :
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;
// Note: toffset, ooffset, or len might be near -1>>>1.
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;
}
該方法同上述方法類似,只是加了一個參數ignoreCase,是否忽略大小寫,如果忽略大小寫則還要將字符轉換成對應的大寫,小寫進行比較。
startsWith(String prefix, int toffset):
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
判斷字符串的子串是否以指定字符開始。如"HelloWorld"就是以指定前綴"Hello"開頭。邏輯是先創建字符串與指定前綴的副本(目的是為了保護字符串和避免其它線程對字符串進行修改導致比較出錯),再從指定位置判斷指定長度,也就是前綴長度字符串內容是否相等。
startsWith(String prefix):
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
同上述方法,只不過該方法的指定位置是從字符串的第0個位置開始。
endsWith:
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
判斷字符串是否以指定字符結尾。調用了startsWith的判斷邏輯。
hashCode:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
重寫Object的hashCode方法。java中的hashCode有兩個作用。一:Object的hashCode返回對象的內存地址。二:對象重寫的hashCode配合基于散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable等。對于大量的元素比較時直接比較equals效率低下,可先判斷hashCode再判斷equals,因為不同的對象可能返回相同的hashCode(如"Aa"和"BB"的hashCode就一樣),所以比較時有時需要再比較equals。hashCode只是起輔助作用。為了使字符串計算出來的hashCode盡可能的少重復,即降低哈希算法的沖突率,設計者選擇了31這個乘數。選31有兩個好處。1:31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一,其它的像37、41、43也是不錯的乘數選擇。2:31可以被 JVM 優化,31 * i = (i << 5) - i。計算hashCode的原理也很簡單,即用原hashCode乘以31再加上char數組的每位值。
indexOf(int ch):
public int indexOf(int ch) {
return indexOf(ch, 0);
}
獲取指定字符在字符串中第一次出現的索引位置。具體調用了indexOf(ch, 0) (見后)。
indexOf(int ch, int fromIndex):
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
獲取指定字符在字符串中指定位置后第一次出現的索引位置。邏輯是:先對開始索引位置fromIndex進行檢查,如果小于0則取0,如果大于數組長度則待查找的結果不存在,返回-1.如果fromIndex合法,再判斷待查找的字符是否是在兩個字節以內。ch < Character.MIN_SUPPLEMENTARY_CODE_POINT 這個條件非常重要,是分界BmpCode的界限,Character.MIN_SUPPLEMENTARY_CODE_POINT這個數代表十進制中62355,剛好是2個字節。如果在兩個字節內則遍歷字符數組找到即返回所引。待查找字符超過兩個字節,則使用indexOfSupplementary(int ch, int fromIndex)方法進行查找。該方法是拆分字符的高低位進行比較,int類型在java中占4個字節,如果不是BmpCode代碼(2字節以內)點是ValidCodePoint(2字節到四字節),代碼點是有高兩位和低兩位,這種類型的int轉化為字符時分開來處理,作為兩個字符。源碼如下。
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
lastIndexOf:
public int lastIndexOf(int ch) {
return lastIndexOf(ch, value.length - 1);
}
public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}
從后住前查找指定字符在字符串中第一次出現的位置。原理同indexOf方法類似,這里就不贅述了。
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
查找指定字符串在原字符串中第一次出現的索引位置。具體實現是調用了indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,int fromIndex)方法。實現如下。
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
該方法是保護的,只能在包內調用。邏輯是(假設是從第0個位置開始找,從其它位置開始邏輯類似):
1.遍歷當前字符串,找到當前字符串中和參數str字符串第一個字符相同的字符的位置記為i。
2.然后逐一比較接下來的每個字符是否相等,如果相等則返回,不等進行3
3.從原字符串第i個位置后找與str第一個字符相等的位置,再比較接下來的每個字符是否相等。
如此循環直到找到,或原字符串遍歷完成結束方法。
lastIndexOf(String str):
public int lastIndexOf(String str) {
return lastIndexOf(str, value.length);
}
從后往前查找字符串str在原字符串中第一次出現的索引位置。該系列方法同indexOf(String str)系列方法邏輯類似。只不過查找順序是從后往前。
substring(int beginIndex):
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
截取字符串的子串。截取從指定位置beginIndex開始(包含這個位置),到字符串結束之間的字符串內容。如果beginIndex=0則返回原串,否則創建一個新的字符串返回。
substring(int beginIndex, int endIndex):
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
截取原字符串的子串,同上個方法類似,上一個方法的結束位置是原串的結尾,這個方法是指定結束位置endIndex(不包含這個位置)。需要注意的是這個方法是包含頭不包含尾。
subSequence:
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
截取指定區間內的字符序列。調用了substring方法,因為String本身就是一個CharSequence,所以這里可以直接返回。
concat:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
連接兩個字符串。先創建了一個新的字符數組復制了兩個字符串中的內容。然后通過String(char[] value, boolean share)創建結果字符串。注意這里用的是直接復制引用的方式而不是復制數組中字符的內容來創建字符串,這可以提高效率,前面寫字符串的構造方法時也提到過。創建的新的字符串,對原來的兩個字符串的內容沒有影響。
replace(char oldChar, char newChar):
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
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;
}
將字符串中所有的舊字符oldChar,替換為新的字符newChar。邏輯是:先找到字符串中第一次出現oldChar字符的位置i。將之前的字符數組復制給新數組buf,然后從i后將字符數組中的內容復制給buf,只不過如果字符為oldCha則替換為newChar.然后再通過buf創建新的字符串返回。
matches:
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
查找字符串是否包含指定正則規則的字符串。關于正則在java也是一個很有用知識點,有興趣的同學可以查一下。
contains:
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
判斷字符串中是否包含指定的字符序列。實際是調用indexOf方法,查找序列在字符串中的位置來判斷的,如果不包含則查找的索引為-1.
replaceFirst:
public String replaceFirst(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
}
替換第一個正則匹配項。需要注意一點,如果需要替換的內容中包含反斜杠\需要用) in the replacement
replaceAll
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
替換所有的正則匹配項。同理新替換的內容中包含反斜杠\需要用$代替。
split(String regex, int limit) :
public String[] split(String regex, int limit) {
char ch = 0;
if (((regex.value.length == 1 && //判斷參數長度是否為1
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || //判斷參數不在特殊符號".$|()[{^?*+\\"中
(regex.length() == 2 && //判斷參數長度是否為2
regex.charAt(0) == '\\' && \\第一位為轉義符"\\"
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && //第二位不是0-9之間 '0'轉換為int為48 '9'轉換為int為57
((ch-'a')|('z'-ch)) < 0 && //判斷不在 a-z之間
((ch-'A')|('Z'-ch)) < 0)) && //判斷不在A-Z之間
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE)) //判斷分隔符不在特殊符號中
{
int off = 0;//當前索引
int next = 0;//下一個分割符出現的索引
boolean limited = limit > 0;//只分割前limit份還是全部分割,limit=0代表全部分割
ArrayList<String> list = new ArrayList<>();//創建一個集合,用于存放切割好的子串
while ((next = indexOf(ch, off)) != -1) {//判斷是否包含下個分隔符,如果有則進入循環
if (!limited || list.size() < limit - 1) {//判斷是全部分割或當前分割次數小于總分割次數
list.add(substring(off, next));//切割當前索引到下一個分隔符之間的字符串并添加到list中
off = next + 1; //繼續切割下一下子串
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));//切割當前索引到字符串結尾的子字符串并添加到list
off = value.length;//將當前索引置為字符串長度
break;//結束循環
}
}
// If no match was found, return this
if (off == 0) //如果找不到分隔符則返回只有本字符串的數組
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)//如果是全部分割,或者沒有達到分割數,則追加最后一項
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {//移除多余集合項
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];//創建對應長度數組,因為返回結果為字符串數組
return list.subList(0, resultSize).toArray(result);//集合轉數組并返回
}
return Pattern.compile(regex).split(this, limit);//其它情況用正則的切割規則去切割
}
根據指定規則切割原字符串。如 "abc,def,ghi".split(",")則返回包含"abc","def","ghi"三個字符串元素的字符串數組。源碼分析直接寫在源碼中。
split:
public String[] split(String regex) {
return split(regex, 0);
}
根據指定規則切割字符串,切割全部子串。
join:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
public static String join(CharSequence delimiter,
Iterable<? extends CharSequence> elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
join方法是jdk1.8之后新加的方法,作用是將字符序列數組,或是字符序列集合通過分割符delimiter連接成一個字符串。提供這兩個實現原理差不多,第一個方法使用的可變參數,第二個方法使用的可迭代參數,這樣設計主要是為了讓方法更好用,參數可以是一個數組也可以是一個集合。再來看下原理。
通過遍歷數組和集合將數組元素或集合元素添加到StringBuilder,添加前會先加入一個分割符delimiter,然后將StringBuilder中的內容返回,具體如下:
//1.StringJoiner的add方法,使用了方法調用鏈的方式,返回對象本身,可重復使用add方法。
public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement);//調用prepareBuilder()包含之前添加的元素和新加入一個分割符,然后再append添加新的元素
return this;
}
//2.StringJoiner的prepareBuilder方法,內部維護了一個StringBuilder
private StringBuilder prepareBuilder() {
if (value != null) {
value.append(delimiter);//每次調用這個方法時會,往StringBuilder中添加分割符delimiter
} else {
value = new StringBuilder().append(prefix);//第一次調用時創建StringBuilder對象
}
return value;//返回StringBuilder對象,以便下次調用的時候操作的是同一個StringBuilder
}
大小寫轉換函數:
public String toLowerCase() {
return toLowerCase(Locale.getDefault());
}
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
從名字可以看出這兩個函數是對字符串進行大小寫轉換的,需要注意的是只是針對英文字母[a-z][A-Z]轉換有效,其它字符轉換無效。
trim:
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
去掉字符串兩端的空白字符,空白字符包括,空格、tab、回車符。邏輯:
1.從左到右循環字符數組,若字符為空字符,則繼續循環,直到第一個不為空的字符記錄其位置st。
2.從右到左循環字符數組,若字符為空字符,則繼續循環,直到第一個不為空的字符記錄其位置len。
3.截取字符串中從st到len位置的子串。
toString:
public String toString() {
return this;
}
返回字符串對象的字符串形式,實際上就是返回他本身。
toCharArray:
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
將字符串轉換為字符數組返回。將字符串中維護的字符數組復制一份返回。這里有兩點需要注意的地方:
1.這里不能直接返回內部字符數組value,如果直接返回 value,返回的數組(假設為chArray)與value指向同一個地址,一旦你修改了 chArray數組的內容,value所指向的內容也隨之改變,這樣破壞了String的不變性。
2.源碼中有一行注釋:Cannot use Arrays.copyOf because of class initialization order issues(由于類初始化順序問題,無法使用ARARY.COSTOFF),這里我猜測是這樣的,字符串比Arrays先初始化完成,但是在JDK中存在其它對象使用了toCharArray方法,而這個對象比String對象初始化晚,但比Arrays對象初始化早,導致使用時Arrays未初始化完成從而報錯。故這里有了這個注釋,而使用 System.arraycopy則不會存在這樣的問題,因為這個方法是本地方法。
format
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
public static String format(Locale l, String format, Object... args) {
return new Formatter(l).format(format, args).toString();
}
用于創建格式化的字符串以及連接多個字符串對象。熟悉C語言的同學應該記得C語言的sprintf()方法,兩者有類似之處。這里給出了兩種重載形式,第一種使用本地語言環境,第二種使用指定的語言環境。
如:String.format("Hi,%s:%s.%s", "z3","l4","w5");返回Hi:z3,l4,w5
valueOf系列:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
valueOf系列,將傳入的參數,轉換成各自對應的字符串對象。需要注意兩點:
1.對于對象如果是null則返回字符串"null".
2.對于boolean類型真返回"true",假返回"false"
intern:
public native String intern();
最后String還有一個intern方法,這個方法是本地方法,無方法體。
jdk1.7之前intern方法執行后如果在常量池找不到對應的字符串,則會將字符串拷貝到常量池,然后返回常量池中的引用。
jdk1.7之后intern方法執行后如果在常量池找不到對應的字符串,則會在常量池中生成一個對原字符串的引用。
原來在常量池中找不到時,復制一個副本放到常量池,1.7后則是將在堆上的地址引用復制到常量池。
實際上在常量池中有一個對象StringTable,可以看作是一個HashSet。使用StringTable來維護所有存活的字符串的一個對象。
使用String的intern方法可以節省內存。在某些情況下可以使用 intern() 方法,它能夠使內存中的不同字符串都只有一個實例對象。
看下面的例子:
public void testIntern() {
String str2 = new String("hello") + new String("world");
str2.intern();// 使用intern方法后str2與str1指向同一個對象,否則它們指向兩個不同的對象。這樣就能達到節省內存的效果。
String str1 = "helloworld";
System.out.println(str2 == str1);
}