歡迎進入第 01 課時的內容“String 的特點是什么?它有哪些重要的方法?”。
幾乎所有的 Java 面試都是以 String 開始的,如果第一個問題沒有回答好,則會給面試官留下非常不好的第一印象,而糟糕的第一印象則會直接影響到自己的面試結果,就好像剛破殼的小鵝一樣,會把第一眼看到的動物當成自己的母親,即使它第一眼看到的是一只小狗或小貓,也會默認跟隨其后,心理學把這種現象叫做印刻效應。印刻效應不僅存在于低等動物之中,同樣也適用于人類,所以對于 String 的知識,我們必須深入的掌握才能為自己贏得更多的籌碼。
本課時的問題是:String 是如何實現的?它有哪些重要的方法?
#### 典型回答
以主流的 JDK 版本 1.8 來說,String 內部實際存儲結構為 char 數組,源碼如下:
```
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 用于存儲字符串的值
private final char value[];
// 緩存字符串的 hash code
private int hash; // Default to 0
// ......其他內容
}
```
String 源碼中包含下面幾個重要的方法。
* [ ] 1. 多構造方法
String 字符串有以下 4 個重要的構造方法:
```
// String 為參數的構造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 為參數構造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 為參數的構造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
// StringBuilder 為參數的構造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
```
其中,比較容易被我們忽略的是以 StringBuffer 和 StringBuilder 為參數的構造函數,因為這三種數據類型,我們通常都是單獨使用的,所以這個小細節我們需要特別留意一下。
* [ ] 2. equals() 比較兩個字符串是否相等
源碼如下:
```
public boolean equals(Object anObject) {
// 對象引用相同直接返回 true
if (this == anObject) {
return true;
}
// 判斷需要對比的值是否為 String 類型,如果不是則直接返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
// 把兩個字符串都轉換為 char 數組對比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循環比對兩個字符串的每一個字符
while (n-- != 0) {
// 如果其中有一個字符不相等就 true false,否則繼續對比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
```
String 類型重寫了 Object 中的 equals() 方法,equals() 方法需要傳遞一個 Object 類型的參數值,在比較時會先通過 instanceof 判斷是否為 String 類型,如果不是則會直接返回 false,instanceof 的使用如下:
```
Object oString = "123";
Object oInt = 123;
System.out.println(oString instanceof String); // 返回 true
System.out.println(oInt instanceof String); // 返回 false
```
當判斷參數為 String 類型之后,會循環對比兩個字符串中的每一個字符,當所有字符都相等時返回 true,否則則返回 false。
還有一個和 equals() 比較類似的方法 equalsIgnoreCase(),它是用于忽略字符串的大小寫之后進行字符串對比。
* [ ] 3. compareTo() 比較兩個字符串
compareTo() 方法用于比較兩個字符串,返回的結果為 int 類型的值,源碼如下:
```
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
// 獲取到兩個字符串長度最短的那個 int 值
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;
}
```
從源碼中可以看出,compareTo() 方法會循環對比所有的字符,當兩個字符串中有任意一個字符不相同時,則 return char1-char2。比如,兩個字符串分別存儲的是 1 和 2,返回的值是 -1;如果存儲的是 1 和 1,則返回的值是 0 ,如果存儲的是 2 和 1,則返回的值是 1。
還有一個和 compareTo() 比較類似的方法 compareToIgnoreCase(),用于忽略大小寫后比較兩個字符串。
可以看出 compareTo() 方法和 equals() 方法都是用于比較兩個字符串的,但它們有兩點不同:
* equals() 可以接收一個 Object 類型的參數,而 compareTo() 只能接收一個 String 類型的參數;
* equals() 返回值為 Boolean,而 compareTo() 的返回值則為 int。
它們都可以用于兩個字符串的比較,當 equals() 方法返回 true 時,或者是 compareTo() 方法返回 0 時,則表示兩個字符串完全相同。
* [ ] 4. 其他重要方法
* indexOf():查詢字符串首次出現的下標位置
* lastIndexOf():查詢字符串最后出現的下標位置
* contains():查詢字符串中是否包含另一個字符串
* toLowerCase():把字符串全部轉換成小寫
* toUpperCase():把字符串全部轉換成大寫
* length():查詢字符串的長度
* trim():去掉字符串首尾空格
* replace():替換字符串中的某些字符
* split():把字符串分割并返回字符串數組
* join():把字符串數組轉為字符串
#### 考點分析
這道題目考察的重點是,你對 Java 源碼的理解,這也從側面反應了你是否熱愛和喜歡專研程序,而這正是一個優秀程序員所必備的特質。
String 源碼屬于所有源碼中最基礎、最簡單的一個,對 String 源碼的理解也反應了你的 Java 基礎功底。
String 問題如果再延伸一下,會問到一些更多的知識細節,這也是大廠一貫使用的面試策略,從一個知識點入手然后擴充更多的知識細節,對于 String 也不例外,通常還會關聯的詢問以下問題:
* 為什么 String 類型要用 final 修飾?
* == 和 equals 的區別是什么?
* String 和 StringBuilder、StringBuffer 有什么區別?
* String 的 intern() 方法有什么含義?
* String 類型在 JVM(Java 虛擬機)中是如何存儲的?編譯器對 String 做了哪些優化?
接下來我們一起來看這些問題的答案。
#### 知識擴展
* [ ] 1. == 和 equals 的區別
== 對于基本數據類型來說,是用于比較 “值”是否相等的;而對于引用類型來說,是用于比較引用地址是否相同的。
查看源碼我們可以知道 Object 中也有 equals() 方法,源碼如下:
```
public boolean equals(Object obj) {
return (this == obj);
}
```
可以看出,Object 中的 equals() 方法其實就是 ==,而 String 重寫了 equals() 方法把它修改成比較兩個字符串的值是否相等。
源碼如下:
```
public boolean equals(Object anObject) {
// 對象引用相同直接返回 true
if (this == anObject) {
return true;
}
// 判斷需要對比的值是否為 String 類型,如果不是則直接返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
// 把兩個字符串都轉換為 char 數組對比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循環比對兩個字符串的每一個字符
while (n-- != 0) {
// 如果其中有一個字符不相等就 true false,否則繼續對比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
```
* [ ] 2. final 修飾的好處
從 String 類的源碼我們可以看出 String 是被 final 修飾的不可繼承類,源碼如下:
```
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence { //...... }
```
那這樣設計有什么好處呢?
Java 語言之父 James Gosling 的回答是,他會更傾向于使用 final,因為它能夠緩存結果,當你在傳參時不需要考慮誰會修改它的值;如果是可變類的話,則有可能需要重新拷貝出來一個新值進行傳參,這樣在性能上就會有一定的損失。
James Gosling 還說迫使 String 類設計成不可變的另一個原因是安全,當你在調用其他方法時,比如調用一些系統級操作指令之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過后,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題,這是迫使 String 類設計成不可變類的一個重要原因。
總結來說,使用 final 修飾的第一個好處是安全;第二個好處是高效,以 JVM 中的字符串常量池來舉例,如下兩個變量:
```
String s1 = "java";
String s2 = "java";
```
只有字符串是不可變時,我們才能實現字符串常量池,字符串常量池可以為我們緩存字符串,提高程序的運行效率,如下圖所示:

試想一下如果 String 是可變的,那當 s1 的值修改之后,s2 的值也跟著改變了,這樣就和我們預期的結果不相符了,因此也就沒有辦法實現字符串常量池的功能了。
* [ ] 3. String 和 StringBuilder、StringBuffer 的區別
因為 String 類型是不可變的,所以在字符串拼接的時候如果使用 String 的話性能會很低,因此我們就需要使用另一個數據類型 StringBuffer,它提供了 append 和 insert 方法可用于字符串的拼接,它使用 synchronized 來保證線程安全,如下源碼所示:
```
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
```
因為它使用了 synchronized 來保證線程安全,所以性能不是很高,于是在 JDK 1.5 就有了 StringBuilder,它同樣提供了 append 和 insert 的拼接方法,但它沒有使用 synchronized 來修飾,因此在性能上要優于 StringBuffer,所以在非并發操作的環境下可使用 StringBuilder 來進行字符串拼接。
* [ ] 4. String 和 JVM
String 常見的創建方式有兩種,new String() 的方式和直接賦值的方式,直接賦值的方式會先去字符串常量池中查找是否已經有此值,如果有則把引用地址直接指向此值,否則會先在常量池中創建,然后再把引用指向此值;而 new String() 的方式一定會先在堆上創建一個字符串對象,然后再去常量池中查詢此字符串的值是否已經存在,如果不存在會先在常量池中創建此字符串,然后把引用的值指向此字符串,如下代碼所示:
```
String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
```
它們在 JVM 存儲的位置,如下圖所示:

小貼士:JDK 1.7 之后把永生代換成的元空間,把字符串常量池從方法區移到了 Java 堆上。
除此之外編譯器還會對 String 字符串做一些優化,例如以下代碼:
```
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2);
```
雖然 s1 拼接了多個字符串,但對比的結果卻是 true,我們使用反編譯工具,看到的結果如下:
```
Compiled from "StringExample.java"
public class com.lagou.interview.StringExample {
public com.lagou.interview.StringExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Java
2: astore_1
3: ldc #2 // String Java
5: astore_2
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
22: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 22
}
```
從編譯代碼 #2 可以看出,代碼 "Ja"+"va" 被直接編譯成了 "Java" ,因此 s1==s2 的結果才是 true,這就是編譯器對字符串優化的結果。
#### 小結
本課時從 String 的源碼入手,重點講了 String 的構造方法、equals() 方法和 compareTo() 方法,其中 equals() 重寫了 Object 的 equals() 方法,把引用對比改成了字符串值對比,也介紹了 final 修飾 String 的好處,可以提高效率和增強安全性,同時我們還介紹了 String 和 JVM 的一些執行細節。
#### 課后問答
* 1、James Gosling,請問這個人說的final修飾的好處能具體講下么?不懂啊
講師回復: 文章有講到,一個是提高了安全性,另一個是提高了效率。
* 2、今天看了一眼源碼,高版string用的byte數組存儲,網上搜了一下,沒找到相關說明,老師有了解么
講師回復: Java 9 之后 String 的存儲就從 char 數組轉成了 byte 數組,這樣做的好處是存儲變的更緊湊,占用的內存更少,操作性能更高了。
* 3、永久代和元空間不都是方法區的實現方式么,JDK1.8將永久代移除,然后加上了元空間,元空間是直接共享使用的機器物理內存,應該是獨立于jvm堆內存的呀,文中談到的字符串常量池移到了堆中,具體是堆中的哪個區域?
講師回復: 元空間確實在本地內存中,這個沒有異議,但本地直接內存中主要存儲的是類型信息,而字符串常量池已經在 JDK 7 時被移除方法區,放到堆中了。你可以將 -Xmx 設置小一些,然后 while 循環調用 String.valueOf(i++).intern() 看 OOM 異常信息 Java heap space 就可以看出字符串常量池已經被移到堆中了,或者是看周志明的《深入理解Java虛擬機》2.4 實戰:OutOfMemoryError 里面有寫。
* 4、String char[] value數組怎么初始化的講講啊,
講師回復: 這個已經超出了字符串的知識點了
* 5、剛剛說到的字符串new的創建方式,不管有沒有肯定先創建這個字符串,然后如果堆中字符串常量池沒有該值則直接放入,是不是少介紹了一種堆中字符串常量池有該值的情況,那么如何處理?是替換原有的?還是舍去新創建的保留原有的?
講師回復: 字符串常量池的作用就是使用已有的字符串來提高程序的執行效率,是復用哦。
* 6、Java 語言之父 James Gosling 的回答是,他會更傾向于使用 final,因為它能夠緩存結果,當你在傳參時不需要考慮誰會修改它的值;如果是可變類的話,則有可能需要重新拷貝出來一個新值進行傳參,這樣在性能上就會有一定的損失。老師,對于這句話我不太理解 final 類在傳參的作用,例如傳參的不是 String,而是一個普通的 Object 類型,它不是 final,傳參的時候是不需要拷貝一個新對象出來的吧
講師回復: 是的可以這樣理解,或者只需要記它的 3 重點優勢:1、可以利用不可變性實現字符串常量池;2、非常適合做 HashMap 的 key(因為不變);3、天生線程安全。
* 7、String 的 intern() 方法有什么含義?老師,貌似這個問題沒有解答吧?
講師回復: String#intern 是一個 native 方法,注釋寫的很詳細,“如果常量池中存在當前字符串, 就會直接返回當前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中后, 再返回”
* 8、有點疑惑,string類是final的與傳參值不變有什么關系啊
講師回復: 意思是,如果是這樣 final 的參數 private static void upString(final String str) { 就不能修改了,而非 fianl 是可以修改的。
* 9、// StringBuffer 為參數的構造方法public String(StringBuffer buffer) { synchronized(buffer) { }}這里加同步代碼塊的原因是StringBuffer是線程安全的原因嗎
講師回復: 這是 JDK 的源碼,原因大概是為了保證線程安全。
* 10、new String() 如果常量池中沒有此字符串的值,那么會在常量池中創建。首先new的時候會在堆中創建對象,如果常量池中沒有這個值,不是直接把剛剛創建的對象的值搬到堆中然后把引用指向它就可以了嗎,為什么還要在常量池中創建一個?
講師回復: new String() 是先去判斷的,還沒有創建對象呢。
* 11、有個地方不清楚String s1 = "Ja" + "va";String s2 = "Java";System.out.println(s1 == s2);如果這樣的話,意思是常量池中不存在"Ja"和"va"嗎?
講師回復: 常量池不會有 “ja”、“va”,代碼在編譯器階段被優化成了"Java"
* 12、大佬,感覺String 和 StringBuilder、StringBuffer 的區別這個是高頻面試題,可以講深一點,文中這樣講面試過不去的??
講師回復: 感謝反饋,建議配合 Java 源碼查看,源碼不是很大,讀完就能做到心中有數了。
* 13、王老師你好,這一張有個問題,在類上面是用final是表示這個類是最終類,不可被繼承。跟實際的內容變化應該沒關系,如果希望string這個變量不可變應該是聲明變量的前面加final。不知道我的理解有沒有誤,希望王老師抽空可以解答一下
講師回復: 對的,你的理解沒問題,final 語義是這樣的。
- 前言
- 開篇詞
- 開篇詞:大廠技術面試“潛規則”
- 模塊一:Java 基礎
- 第01講:String 的特點是什么?它有哪些重要的方法?
- 第02講:HashMap 底層實現原理是什么?JDK8 做了哪些優化?
- 第03講:線程的狀態有哪些?它是如何工作的?
- 第04講:詳解 ThreadPoolExecutor 的參數含義及源碼執行流程?
- 第05講:synchronized 和 ReentrantLock 的實現原理是什么?它們有什么區別?
- 第06講:談談你對鎖的理解?如何手動模擬一個死鎖?
- 第07講:深克隆和淺克隆有什么區別?它的實現方式有哪些?
- 第08講:動態代理是如何實現的?JDK Proxy 和 CGLib 有什么區別?
- 第09講:如何實現本地緩存和分布式緩存?
- 第10講:如何手寫一個消息隊列和延遲消息隊列?
- 模塊二:熱門框架
- 第11講:底層源碼分析 Spring 的核心功能和執行流程?(上)
- 第12講:底層源碼分析 Spring 的核心功能和執行流程?(下)
- 第13講:MyBatis 使用了哪些設計模式?在源碼中是如何體現的?
- 第14講:SpringBoot 有哪些優點?它和 Spring 有什么區別?
- 第15講:MQ 有什么作用?你都用過哪些 MQ 中間件?
- 模塊三:數據庫相關
- 第16講:MySQL 的運行機制是什么?它有哪些引擎?
- 第17講:MySQL 的優化方案有哪些?
- 第18講:關系型數據和文檔型數據庫有什么區別?
- 第19講:Redis 的過期策略和內存淘汰機制有什么區別?
- 第20講:Redis 怎樣實現的分布式鎖?
- 第21講:Redis 中如何實現的消息隊列?實現的方式有幾種?
- 第22講:Redis 是如何實現高可用的?
- 模塊四:Java 進階
- 第23講:說一下 JVM 的內存布局和運行原理?
- 第24講:垃圾回收算法有哪些?
- 第25講:你用過哪些垃圾回收器?它們有什么區別?
- 第26講:生產環境如何排除和優化 JVM?
- 第27講:單例的實現方式有幾種?它們有什么優缺點?
- 第28講:你知道哪些設計模式?分別對應的應用場景有哪些?
- 第29講:紅黑樹和平衡二叉樹有什么區別?
- 第30講:你知道哪些算法?講一下它的內部實現過程?
- 模塊五:加分項
- 第31講:如何保證接口的冪等性?常見的實現方案有哪些?
- 第32講:TCP 為什么需要三次握手?
- 第33講:Nginx 的負載均衡模式有哪些?它的實現原理是什么?
- 第34講:Docker 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?