前言
最近花了兩天時間,整理了一下String的源碼。這個整理並不全面但是也涵蓋了大部分Spring源碼中的方法。後續如果有時間還會將剩餘的未整理的方法更新到這篇文章中。方便以後的複習和麪試使用。如果文章中有地方有問題還請指出。
簡述
字符串廣泛應用 在 Java 編程中,在 Java 中字符串屬於對象,Java 提供了
String
類來創建和操作字符串。字符串緩衝區支持可變字符串。因爲String
對象是不可變的,因此可以共享它們。
String
類代表字符串,Java程序中的所有字符串字面值如"abc"
都是這個類的實例對象。String
類是不可改變的,所以你一旦創建了String
對象,那它的值就無法改變了。如果需要對字符串做很多修改,那麼應該選擇使用StringBuilder
或者StringBuffer
。
最簡單的創建字符串的方式:
String qc = "qiu chan"
編譯器會使用該值創建一個 對象。我們也可以使用關鍵字New
創建String
對象。
String
類型的常量池比較特殊。它的主要使用方法有兩種:
- 直接使用雙引號聲明出來的
String
對象會直接存儲在常量池中。- 如果不是用雙引號聲明的
String
對象,可以使用String
提供的intern
方法。intern 方法
會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。
繼承/實現關係
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 省略
}
String是final修飾
的不能夠被繼承和修改。
源碼
String的底層使用的是char數組用於存儲。
private final char value[];
緩存字符串的哈希碼默認值爲0
private int hash;
無參數構造函數
public String() {
this.value = "".value;
}
解析:初始化一個新創建的
String
對象,使其代表一個空字符序列。 注意,由於String
是不可變的,所以不需要使用這個構造函數。
參數爲字符串的構造函數
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
解析:初始化一個新創建的
String
對象,使其代表與參數相同的字符序列。換句話說,新創建的字符串是參數字符串的副本。除非需要參數字符串的顯式拷貝,否則不需要使用這個構造函數,因爲String
是不可變的。
參數爲char數組的構造函數
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
解析:分配一個新的
String
,使其代表當前字符數組參數中包含的字符序列。使用Arrays.copyOf
方法進行字符數組的內容被複制。字符數組的後續修改不會影響新創建的字符串。
參數爲char數組並且帶有偏移量的構造方法
// value[]:作爲字符源的數組,offset:偏移量、下標從0開始並且包括offset,count:從數組中取到的元素的個數。
public String(char value[], int offset, int count) {
// 如果偏移量小於0拋出IndexOutOfBoundsException異常
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
// 判斷要取的元素的個數是否小於等於0
if (count <= 0) {
// 要取的元素的個數小於0,拋出IndexOutOfBoundsException異常
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// 在要取的元素的個數等於0的情況下,判斷偏移量是否小於等於數組的長度
if (offset <= value.length) {
// 偏移量小於等於數組的長度,返回一個空字符串數組的形式
this.value = "".value;
return;
}
}
// 如果偏移量的值大於數組的長度減去取元素的個數拋出IndexOutOfBoundsException異常
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 複製元素
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
解析:分配一個新的
Sting
,來源於給定的char數組
中的字符。offset
參數是子數組中第一個字符的索引,count
參數指定子數組的長度。子數組被被複制以後,對字符數組的修改不會影響新創建的字符串。
參數爲StringBuffer的構造方法
public String(StringBuffer buffer) {
// 這裏對StringBuffer進行了加鎖,然後再進行拷貝操作。這裏對其進行加鎖正是爲了保證在多線程環境下只能有一個線程去操作StringBuffer對象。
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
解析:分配一個新的字符串,該字符串包含當前字符串緩衝區參數中包含的字符序列。
Arrays.copyOf
方法進行字符串緩衝區中內容的複製。這裏對StringBuffer
進行了加鎖,然後再進行拷貝操作。這裏對其進行加鎖正是爲了保證在多線程環境下只能有一個線程去操作StringBuffer
對象。
參數爲StringBuilder的構造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
解析:參數是
StringBuilder
,這個是線程不安全的,但是性能相對於StringBuffer
有很大的提升,源碼的註釋中說通過toString
方法從字符串構建器中獲取字符串可能會運行得更快,通常是首選。
length方法
public int length() {
// 查看源碼發現,這個value是一個char數組,本質獲取的是字符串對應的char數組的長度。
return value.length;
}
解析:返回此字符串的長度。查看源碼發現,這個
value
是一個char數組
,本質獲取的是字符串對應的char數組
的長度。
isEmpty方法
public boolean isEmpty() {
// 底層的char數組的長度是否爲0進行判斷
return value.length == 0;
}
//舉例
@Test
public void test_string_isEmpty(){
System.out.println(" ".isEmpty());// false
System.out.println("".isEmpty());// true
}
解析:判斷給定的字符串是否爲空,底層實現是根據
char
數組的長度是否爲0
進行判斷。
charAt方法
public char charAt(int index) {
// 給定的索引小於0或者給定的索引大於這個字符串對應的char數組的長度拋出角標越界異常
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
// 獲取當前的指定位置的char字符
return value[index];
}
解析:根據給定的索引獲取當前的指定位置的
char
字符。如果給定的索引否小於0
,或者給定的索引是大於這個字符串對應的char數組的長度拋出角標越界異常。index
是從0
開始到length-1
結束。序列的第一個char
值在索引0
處,下一個在索引1處,依此類推,與數組索引一樣。
getChars方法
// srcBegin:要複製的字符串中第一個字符的索引【包含】。srcEnd:要複製的字符串中最後一個字符之後的索引【不包含】。dst[]:目標數組。dstBegin:目標數組中的起始偏移量。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
// 校驗起始索引小於0拋出角標越界異常
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);
}
// 案例
@Test
public void test_string_codePointAt(){
// 原始字符串
String h = "ahelloworld";
// 目標char數組
char[] data = new char[4];
// 執行拷貝
h.getChars(2, 6, data, 0);
System.out.println(data);
}
解析:將字符串中的字符複製到目標字符數組中。索引包含
srcBegin
,不包含srcEnd
。
equals方法
// anObject:與此String進行比較的對象。
public boolean equals(Object anObject) {
// 引用相同直接返回true
if (this == anObject) {
return true;
}
// 判斷給定的對象是否是String類型的
if (anObject instanceof String) {
// 給定的對象是字符串類型的轉換爲字符串類型
String anotherString = (String)anObject;
// 獲取當前字符串的長度
int n = value.length;
// 判斷給定字符串的長度是否等於當前字符串的長度
if (n == anotherString.value.length) {
// v1[]代表當前字符串對應的char數組
char v1[] = value;
// v2[]代表給定的字符串對應的char數組
char v2[] = anotherString.value;
// 遍歷原始char數組,並且與給定的字符串對應的數組進行比較
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
// 任意一個位置上不相等返回false
return false;
i++;
}
// 都相等返回true
return true;
}
}
// 不是String類型,或者長度不一致返回false
return false;
}
解析:這個方法重寫了
Object
中的equals
方法。方法中的將此字符串與指定對象進行比較。接下來附贈一個手寫的String
字符串equals
方法。
手寫equals方法
private boolean mineEquals(String srcObject, Object anObject){
// 比較引用是否相同
if (srcObject == anObject){
return true;
}
// 引用不相同比較內容
if (anObject instanceof String){
String ans = (String) anObject;
char[] srcChar = srcObject.toCharArray();
char[] anChar = ans.toCharArray();
int n = srcChar.length;
if (n == anChar.length){
int i = 0;
while (n-- != 0){
if (srcChar[i] != anChar[i])
return false;
i++;
}
return true;
}
}
return false;
}
// 測試我們自己寫的equals方法
@Test
public void test_string_mine(){
String s = new String("aaa");
// 走的是引用的比較
System.out.println(s.equals(s));// true
boolean b = mineEquals(s, s);
System.out.println(b);// true
}
equalsIgnoreCase方法
public boolean equalsIgnoreCase(String anotherString) {
// 引用相同返回true。引用不相同進行長度、各個位置上的char是否相同
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
解析:將此字符串與另一個字符串進行比較,而忽略大小寫注意事項。
regionMatches
方法的源碼很有趣的,源碼裏面有一個while循環,先進行未忽略大小的判斷,然後進行忽略大小的判斷,在忽略大小的判斷中,先進行的是大寫的轉換進行比較,但是可能會失敗【這種字體Georgian alphabet
】。所以在大寫轉換以後的比較失敗,進行一次小寫的轉換比較。
startsWith方法
// 判斷是否以指定的前綴開頭
public boolean startsWith(String prefix) {
// 0代表從開頭進行尋找
return startsWith(prefix, 0);
}
endsWith方法
// 判斷是否以指定的前綴結尾
public boolean endsWith(String suffix) {
// 從【value.length - suffix.value.length】開始尋找,這個方法調用的還是startsWith方法
return startsWith(suffix, value.length - suffix.value.length);
}
startsWith和endsWith最終的實現方法
// prefix: 測試此字符串是否以指定的前綴開頭。toffset: 從哪裏開始尋找這個字符串。
public boolean startsWith(String prefix, int toffset) {
// 原始的字符串對應的char[]
char ta[] = value;
// 開始尋找的位置
int to = toffset;
// 獲取指定的字符串對應的char[]
char pa[] = prefix.value;
int po = 0;
// 獲取指定的字符串對應的char[]長度
int pc = prefix.value.length;
// 開始尋找的位置小於0,或者起始位置大於要查找的長度【value.length - pc】返回false。
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
// 比較給定的字符串的char[]裏的每個元素是否跟原始的字符串對應的char數組的元素相同
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
// 有一個char不相同返回false
return false;
}
}
// 相同返回true
return true;
}
substring方法
// 返回一個字符串,該字符串是該字符串的子字符串。beginIndex開始截取的索引【包含】。
public String substring(int beginIndex) {
// 校驗指定的索引,小於0拋出角標越界
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 子字符串的長度
int subLen = value.length - beginIndex;
// 子字符串的長度小於0拋出角標越界
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 開始位置爲0,返回當前字符串,不爲0,創建一個新的子字符串對象並返回
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
解析:返回一個字符串,該字符串是該字符串的子字符串。子字符串以指定索引處的字符開頭【包含】,並且擴展到該字符串的末尾。
substring方法
// beginIndex:開始位置【包含】。 endIndex:結束位置【不包含】。
public String substring(int beginIndex, int endIndex) {
// 校驗指定的開始索引,小於0拋出角標越界
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 校驗指定的結束索引,大於給定的字符串的char數組的長度拋出角標越界
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
// 要截取的長度
int subLen = endIndex - beginIndex;
// 要截取的長度小於0,拋出角標越界
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 截取字符串
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
解析:返回一個字符串,該字符串是該字符串的子字符串。子字符串從指定的
beginIndex
開始【包含】,並且擴展到索引endIndex-1
處的字符【不包含】。
concat方法
public String concat(String str) {
// 獲取給定的字符串的長度
int otherLen = str.length();
// 長度爲0,直接返回當前的字符串
if (otherLen == 0) {
return this;
}
// 獲取當前字符串的長度
int len = value.length;
// 構建一個新的長度爲len + otherLen的字符數組,並且將原始的數據放到這個數組
char buf[] = Arrays.copyOf(value, len + otherLen);
// 這個底層調用是System.arraycopy這個方法的處理是使用c語言寫的
str.getChars(buf, len);
return new String(buf, true);
}
將指定的字符串連接到該字符串的末尾。字符串拼接。
format方法
// 使用指定的格式字符串和參數返回格式化的字符串。
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
// 案例,這裏是使用%s替換後面的如"-a-"
@Test
public void test_start(){
System.out.println(String.format("ha %s hh %s a %s h", "-a-", "-b-", "-c-"));
}
trim方法
public String trim() {
// 指定字符串的長度
int len = value.length;
// 定義一個開始位置的索引0
int st = 0;
// 定義一個char[] val,用於避免使用getfiled操作碼,這個可以寫段代碼反編譯一下看看
char[] val = value;
// 對於字符串的開頭進行去除空格,並記錄這個索引
while ((st < len) && (val[st] <= ' ')) {
st++;
}
// 對於字符串的尾部進行去除空格,也記錄這個索引,這個索引就是去除尾部空格後的索引
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
// 根據上面記錄的長度判斷是否要截取字符串
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
返回一個字符串,其值就是這個字符串,並去掉任何首部和尾部的空白。
join方法
// 返回一個新的String,該字符串由給定的分隔符和要連接的元素組成。delimiter:分隔每個元素的分隔符。elements:連接在一起的元素。
public static String join(CharSequence delimiter, CharSequence... elements) {
// delimiter和elements爲空拋出空指針異常,null會被攔截,""不會被攔截
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
//
StringJoiner joiner = new StringJoiner(delimiter);
// 遍歷給定的要拼接的元素,拼接的元素允許爲null
for (CharSequence cs: elements) {
// 執行拼接方法
joiner.add(cs);
}
return joiner.toString();
}
// 拼接方法
public StringJoiner add(CharSequence newElement) {
// prepareBuilder()方法首次調用會創建StringBuilder對象,後面再調用會執行拼接分隔符
prepareBuilder().append(newElement);
return this;
}
// 未進行拼接創建StringBuilder對象,已經拼接以後value != null執行拼接分隔符
private StringBuilder prepareBuilder() {
// 判斷拼接的value是否爲空
if (value != null) {
// 不爲空執行拼接分隔符
value.append(delimiter);
} else {
// 最開始使用拼接的時候,調用這個方法創建一個空的StringBuilder對象,只調一次
value = new StringBuilder().append(prefix);
}
return value;
}
// 上面是調用的這個拼接元素方法
@Override
public StringBuilder append(CharSequence s) {
// 這裏啥都沒處理,調用的是父類的append方法,設計模式爲建造者模式
super.append(s);
return this;
}
// 上面的prepareBuilder方法是拼接分隔符,這個方法是將分隔符和給定的元素拼接的方法
@Override
public AbstractStringBuilder append(CharSequence s) {
// 以下3個判斷根據類型和是否爲空進行區別拼接
if (s == null)
return appendNull();
if (s instanceof String)
return this.append((String)s);
if (s instanceof AbstractStringBuilder)
return this.append((AbstractStringBuilder)s);
// 拼接
return this.append(s, 0, s.length());
}
將給定的字符串以給定的分割符分割並返回分隔後的字符串。
replace方法
// target:要被替換的目標字符串。 replacement:替換的字符串
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
解析:用指定的字符串替換這個字符串中與之匹配的每個子字符串。替換從字符串的開頭到結尾,例如,在字符串 "aaa "中用 "b "替換 "aa "將導致 "ba "而不是 “ab”。
replaceAll方法
// regex:這個支持正則表達式,也可以是要被替換的目標字符串。
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
問題:
replace
和replaceAll
方法的區別是啥?
replaceAll
支持正則表達式。
針對char的replace方法
// oldChar:要被替換的字符,newChar:替換的字符
public String replace(char oldChar, char newChar) {
// oldChar不等於newChar
if (oldChar != newChar) {
// 當前字符串的長度
int len = value.length;
// 這個用於下面的while循環裏的條件比較,val[i]中的i是從0開始的
int i = -1;
// 定義一個char[] val,用於避免使用getfiled操作碼,這個可以寫段代碼反編譯一下看看
char[] val = value; /* avoid getfield opcode */
// 這個用於記錄這個i的值,並且判斷是否有要替換的,這個循環有利於性能的提升
while (++i < len) {
// val[i]中的i是從0開始的
if (val[i] == oldChar) {
// 有要替換的直接跳出循環
break;
}
}
// 上面的while循環中如果有要替換的i肯定小於len,如果沒有下面這個判斷就不會執行
if (i < len) {
// 能進到這個循環肯定是有要替換的,創建一個長度爲len的char數組
char buf[] = new char[len];
// 上面的i是記錄第一個可以替換的char的索引,下面這個循環是將這個i索引前的不需要被替換的填充到buf[]數組中
for (int j = 0; j < i; j++) {
// 填充buf[]數組
buf[j] = val[j];
}
// 從可以替換的索引i開始將剩餘的字符一個一個填充到 buf[]中
while (i < len) {
// 獲取要被替換的字符
char c = val[i];
// 判斷這個字符是否真的需要替換,c == oldChar成立就替換,否則不替換
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 返回替換後的字符串
return new String(buf, true);
}
}
// oldChar等於newChar直接返回當前字符串
return this;
}
案例
@Test
public void test_matches(){
String a = "adddfdefe";
System.out.println(a.replace('d', 'b'));// abbbfbefe
}
仿寫replace方法參數針對char
仿寫
// 和源碼給的唯一不同的是參數傳遞,其他的都和源碼一樣,自己寫一遍可以加深記憶和借鑑編程思
public String replace(String source, char oldChar, char newChar) {
char[] value = source.toCharArray();
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);
}
}
return new String(value);
}
intern方法
public native String intern();
這是一個
native
方法。調用String#intern
方法時,如果池中已經包含一個由equals
方法確定的等於此String
對象的字符串,則返回來自池的字符串。否則,將此String
對象添加到池中,並返回這個String
的引用。