一文系統(tǒng)梳理Java中字符串比較的高頻誤區(qū)
前言
很多 Java 初學(xué)者第一次寫字符串比較,都會下意識使用 ==。更麻煩的是,有些代碼用 == 居然也能得到 true,于是問題變得更迷惑:字符串到底能不能用 == 比?equals() 一定是比較內(nèi)容嗎?new String("hello") 和 "hello" 到底差在哪?本文會從 ==、equals()、字符串常量池、字符串拼接和 intern() 五個角度,把這些高頻坑一次理順。
一、先給結(jié)論:字符串比較別靠感覺
先把最關(guān)鍵的結(jié)論放在前面:
- 基本類型:== 比較值
- 引用類型:== 比較是否同一個對象
- equals:普通方法,默認也比較引用
- String.equals:String 重寫后的方法,比較字符串內(nèi)容
- 字符串字面量:會和字符串常量池有關(guān)
- new String(...):會創(chuàng)建新的 String 對象
所以,看到下面這兩行代碼時,不能只盯著內(nèi)容是不是一樣:
String a = "hello";
String b = new String("hello");
它們的內(nèi)容一樣,但不代表它們一定是同一個對象。
| 寫法 | 比較內(nèi)容 | 比較對象是否相同 | 是否推薦用于字符串內(nèi)容比較 |
|---|---|---|---|
a == b | 否 | 是 | 不推薦 |
a.equals(b) | 是,前提是 a 不為 null | 否 | 推薦 |
"hello".equals(a) | 是 | 否 | 推薦,可避免空指針 |
核心結(jié)論: 字符串內(nèi)容比較使用 equals(),不要因為某些字面量場景下 == 返回 true,就誤以為 == 在比較字符串內(nèi)容。

二、==到底比較什么
2.1 基本類型:比較值本身
如果兩邊都是基本類型,== 比較的是值。
基本類型使用 == 比較示例
int a = 10; int b = 10; System.out.println(a == b); // true
這里沒有對象,也沒有引用,變量里保存的就是具體數(shù)值。
2.2 引用類型:比較是否指向同一個對象
如果兩邊是引用類型,== 比較的是兩個引用是否指向同一個對象。
引用類型使用 == 比較示例
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false
s1 和 s2 的內(nèi)容都是 "hello",但它們是兩個不同的 String 對象,所以 == 返回 false。
可以把引用先粗略理解成對象的“地址牌”:
s1 ---> String 對象 A,內(nèi)容是 hello
s2 ---> String 對象 B,內(nèi)容是 hello
內(nèi)容一樣,不代表地址牌一樣。
2.3 工程結(jié)論:字符串內(nèi)容用equals()
對引用類型來說,== 只回答一個問題:這兩個引用是不是指向同一個對象?
它不會逐字符比較字符串內(nèi)容。
所以業(yè)務(wù)代碼里判斷輸入、命令、狀態(tài)碼、配置值這類文本內(nèi)容時,直接用 equals(),不要用 == 賭常量池。
誤區(qū):== 不能比較字符串
正確理解: == 可以比較字符串引用,但它比較的是是否同一個對象。某些字面量場景下 == 返回 true,只是因為兩個引用剛好指向同一個池中對象,不代表 == 是字符串內(nèi)容比較工具。
三、equals()不是天生比較內(nèi)容
3.1equals()是Object里的方法
Java 中所有類最終都繼承自 Object。
Object 中有一個 equals(Object obj) 方法。默認情況下,它的行為和 == 很像,也是判斷兩個引用是否指向同一個對象。
普通類未重寫 equals() 示例
class Student {
private String name;
public Student(String name) {
this.name = name;
}
}
Student s1 = new Student("Tom");
Student s2 = new Student("Tom");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // false
兩個學(xué)生對象的 name 都是 "Tom",但 Student 沒有重寫 equals(),所以這里仍然按對象引用來比較。
核心結(jié)論: equals() 不是語法規(guī)則,它只是一個方法。這個方法到底怎么比較,取決于類本身有沒有重寫它。
3.2String重寫了equals()
String 的特殊之處在于:它重寫了 equals()。
所以:
String 使用 equals() 比較內(nèi)容示例
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
s1 == s2 為 false,因為它們不是同一個對象。
s1.equals(s2) 為 true,因為 String.equals() 比較的是字符串內(nèi)容。
入門階段不需要背源碼,只要知道它的大致邏輯:
- 如果兩個引用本來就是同一個對象,直接返回 true。
- 如果對方不是 String,返回 false。
- 如果對方也是 String,再比較字符串內(nèi)容是否一致。
在不同 JDK 中,String 內(nèi)部存儲細節(jié)可能不同,比如早期常見 char[],后續(xù)版本可能使用更緊湊的 byte[]。但對我們使用者來說,結(jié)論不變:String.equals() 比較字符串內(nèi)容。
3.3StringBuilder的equals()是一個反例
不要把 String.equals() 的規(guī)則套到所有字符串相關(guān)類上。
StringBuilder 和 StringBuffer 沒有像 String 那樣重寫 equals(),所以它們的 equals() 仍然是引用比較。
StringBuilder 比較示例
StringBuilder sb1 = new StringBuilder("abc");
StringBuilder sb2 = new StringBuilder("abc");
System.out.println(sb1 == sb2); // false
System.out.println(sb1.equals(sb2)); // false
System.out.println(sb1.toString().equals(sb2.toString())); // true
如果想比較兩個 StringBuilder 里的文本內(nèi)容,可以先轉(zhuǎn)成 String,再調(diào)用 String.equals()。
四、字符串常量池:為什么有時==也會返回 true
4.1 字符串字面量會被復(fù)用
看這段代碼:
字符串字面量比較示例
String a = "hello"; String b = "hello"; System.out.println(a == b); // true
這段代碼里,a == b 返回 true。
但注意,它不是因為 == 突然開始比較內(nèi)容了,而是因為兩個引用指向了同一個字符串對象。
字符串字面量 "hello" 會和字符串常量池有關(guān)??梢韵劝炎址A砍乩斫獬?JVM 管理的一塊字符串緩存:
- 字符串字面量和編譯期常量表達式的結(jié)果會被 intern。
- 相同內(nèi)容的字符串字面量會引用同一個 String 對象。
這里不是“盡量復(fù)用”,而是 Java 語言規(guī)范對字符串字面量共享語義的保證。具體什么時候解析、怎么放入池中,可以交給 JVM 實現(xiàn)處理;對 Java 代碼來說,相同內(nèi)容的字符串字面量會指向同一個池中對象。
所以:
a ---> 常量池中的 "hello"
b ---> 常量池中的 "hello"
兩個引用指向同一個對象,a == b 才返回 true。
4.2new String("hello")會創(chuàng)建新對象
再看這段:
字面量與 new String 對比示例
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
a 指向的是常量池相關(guān)的 "hello" 對象。
b 指向的是 new 出來的新 String 對象。
它們內(nèi)容一樣,但不是同一個對象。
這里還有一個經(jīng)常被說錯的點:
new String("hello") 至少會創(chuàng)建一個新的 String 對象。至于常量池中的 "hello" 是否在這一行新建,取決于池中之前是否已經(jīng)有它。
所以,不要死記成“new String("hello") 一定創(chuàng)建兩個對象”。更嚴謹?shù)恼f法是:new String("hello") 一定會創(chuàng)建一個新的堆中 String 對象;字符串字面量 "hello" 會關(guān)聯(lián)字符串常量池。
4.3 字符串常量池不等于元空間
這里順手把一個 JVM 概念坑捋清楚。
很多資料會同時提到:
- Class 文件常量池
- 運行時常量池
- 字符串常量池
- 方法區(qū)
- 永久代
- 元空間
這些詞很容易混在一起。
入門階段先記住下面這張表:
| 名稱 | 入門理解 | 本文是否重點 |
|---|---|---|
| Class 文件常量池 | .class 文件中的常量表 | 不是 |
| 運行時常量池 | 類加載后,常量池在運行時的形態(tài) | 不是 |
| 字符串常量池 | JVM 用來復(fù)用字符串對象引用的結(jié)構(gòu) | 是 |
在 HotSpot JVM 中:
- JDK 6:字符串常量池主要在永久代中。
- JDK 7+:字符串常量池移動到了堆中。
- JDK 8+:永久代被移除,類元數(shù)據(jù)主要放到元空間中;但字符串常量池不是“搬到元空間”。
核心結(jié)論: 本文討論的是字符串常量池,也就是字符串字面量和 intern() 相關(guān)的那部分,不要把它和方法區(qū)、元空間直接畫等號。
五、字符串拼接:編譯期和運行期不是一回事
5.1 字面量拼接會被編譯器折疊
下面這段代碼很容易讓人誤判:
字符串字面量拼接示例
String a = "ab"; String b = "a" + "b"; System.out.println(a == b); // true
為什么是 true?
根據(jù) Java 語言規(guī)范,"a" + "b" 是編譯期常量表達式,編譯器必須在編譯時計算出 "ab"。
也就是說,源碼看起來像拼接,但 b 在字節(jié)碼里直接對應(yīng)的就是 "ab":
String b = "ab";
所以 a 和 b 最終都指向常量池里的同一個 "ab"。
5.2 變量參與拼接通常是運行期結(jié)果
再看這段:
變量參與字符串拼接示例
String x = "a";
String y = "b";
String z = x + y;
System.out.println("ab" == z); // false
System.out.println("ab".equals(z)); // true
x + y 需要在程序運行時計算,結(jié)果通常是一個新的字符串對象,不會自動等同于常量池中的 "ab" 引用。
老版本字節(jié)碼中,運行期字符串拼接常表現(xiàn)為 StringBuilder;JDK 9 以后,底層可能使用 invokedynamic 和 StringConcatFactory。這些實現(xiàn)細節(jié)入門階段不用展開。
對工程判斷來說,關(guān)鍵推論不變:無論底層是 StringBuilder 還是 invokedynamic,運行期拼接產(chǎn)生的結(jié)果都不能依賴 == 與常量池對象相等。
只要抓住一句話:
- 字面量拼接屬于編譯期常量表達式,會在編譯期計算。
- 變量參與拼接通常是運行期結(jié)果。
- 判斷內(nèi)容是否相同,仍然用 equals()。
5.3final常量的特殊情況
如果變量是編譯期常量,也可能被編譯器提前折疊。
final 編譯期常量拼接示例
final String x = "a";
final String y = "b";
String z = x + y;
System.out.println("ab" == z); // true
這里 x 和 y 都是編譯期可以確定的常量,所以 x + y 可以被優(yōu)化成 "ab"。
更具體一點,能被編譯器折疊的 String 變量通常要滿足這些條件:
- 使用 final 修飾。
- 聲明時直接用字符串字面量或其他編譯期常量表達式初始化。
- 編譯器在編譯階段就能確定它的值。
反過來,如果 final 變量的值來自運行期對象創(chuàng)建,就不能按編譯期常量處理。
final 但不能編譯期折疊的示例
final String x = new String("a");
final String y = "b";
String z = x + y;
System.out.println("ab" == z); // false
這里 x 雖然是 final,但 new String("a") 是運行期創(chuàng)建對象,不是編譯期常量表達式。

六、intern():手動拿到池中的引用
6.1intern()的基本作用
intern() 可以理解成:返回字符串常量池中與當(dāng)前字符串內(nèi)容相同的那個規(guī)范引用。
intern 基本示例
String a = new String("hello");
String b = a.intern();
String c = "hello";
System.out.println(a == b); // false
System.out.println(b == c); // true
這里可以這樣理解:
- a 指向 new 出來的 String 對象。
- b 是 intern() 返回的池中引用。
- c 是字面量引用,也指向池中的 "hello"。
所以 b == c 為 true。

6.2intern()不適合死背特殊輸出
網(wǎng)上經(jīng)常會看到這種題:
intern 進階案例
String s1 = new StringBuilder().append("think").append("123").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder().append("ja").append("va").toString();
System.out.println(s2.intern() == s2);
以常見 HotSpot JDK 8 環(huán)境為例,很多時候會看到這樣的結(jié)果:
true
false
為什么第一個可能是 true?
new StringBuilder().append("think").append("123").toString() 會在運行期生成一個內(nèi)容為 "think123" 的新 String 對象。通常情況下,字符串常量池里原本沒有 "think123"。在 JDK 7+ 的 HotSpot 中,字符串常量池已經(jīng)在堆中,調(diào)用 s1.intern() 時,池里如果沒有這個內(nèi)容,就可能把當(dāng)前這個堆中字符串對象的引用放進去。因此:
- s1.intern() 返回的就是 s1 指向的對象
- 所以 s1.intern() == s1 為 true
為什么第二個可能是 false?
"java" 這個字符串比較特殊。在 JDK 8 等環(huán)境中,它很可能在當(dāng)前代碼執(zhí)行前,已經(jīng)被 JVM 或類庫內(nèi)部提前放入字符串常量池。此時 s2 是運行期新創(chuàng)建的對象,而 s2.intern() 返回的是池里早就存在的那個 "java" 引用。因此:
- s2 指向新對象
- s2.intern() 返回舊的池中對象
- 所以 s2.intern() == s2 為 false
這段例子真正想說明的不是“永遠背 true、false”,而是下面這個判斷步驟:
更適合記住的是規(guī)則:
- intern() 返回池中內(nèi)容相同字符串的引用。
- 如果池中已經(jīng)有,返回已有引用。
- 如果池中沒有,會嘗試把當(dāng)前字符串對應(yīng)內(nèi)容放入池中,再返回池中的引用。
JDK 6、JDK 7+、JDK 11/12 以后,以及不同啟動流程下,某些字符串是否提前進入池中可能不同。所以這類題要按規(guī)則分析,不要只背輸出。
6.3 實戰(zhàn)中不要亂用intern()
intern() 不是性能優(yōu)化萬能藥。
字符串常量池本質(zhì)上也需要管理和查找。如果盲目把大量動態(tài)字符串都 intern(),可能帶來額外內(nèi)存占用和查找成本。
普通業(yè)務(wù)代碼里,優(yōu)先做到:
- 字符串內(nèi)容比較用
equals()。 - 不要用
==判斷字符串內(nèi)容。 - 真正需要大量重復(fù)字符串去重時,再評估是否使用
intern()或其他更合適的緩存方案。
七、為什么String要設(shè)計成不可變
字符串常量池能安全復(fù)用,有一個重要前提:String 是不可變的。
假設(shè) String 可以被隨便修改,會發(fā)生什么?
String a = "hello"; String b = "hello";
如果 a 和 b 指向同一個常量池對象,而 a 可以把內(nèi)容改成 "java",那 b 看到的內(nèi)容也會被影響。這顯然很危險。
所以,String 不可變帶來幾個好處:
| 好處 | 說明 |
|---|---|
| 支持常量池復(fù)用 | 多個引用可以安全共享同一個字符串對象 |
| 線程更安全 | 不可變對象天然更適合多線程共享 |
| 適合作為 Map 的 key | 內(nèi)容不變,哈希值穩(wěn)定 |
| 便于緩存 hashCode | 字符串內(nèi)容不變,計算過的哈希值可以復(fù)用 |
這也是為什么后面學(xué)習(xí) HashMap、HashSet 時,經(jīng)常會看到 String 被當(dāng)作 key 或去重元素。
核心結(jié)論: String 的不可變性不是擺設(shè),它是字符串常量池、線程安全和哈希結(jié)構(gòu)穩(wěn)定性的基礎(chǔ)。
八、幾個真實容易踩的坑
8.1 用==判斷字符串內(nèi)容
錯誤示例
String input = new String("yes");
if (input == "yes") {
System.out.println("通過");
}
這段代碼不可靠,因為 input 和 "yes" 不一定指向同一個對象。
正確寫法
String input = new String("yes");
if ("yes".equals(input)) {
System.out.println("通過");
}
把常量字符串寫在前面,可以避免 input 為 null 時調(diào)用 input.equals(...) 拋出 NullPointerException。
8.2 忽略equals()區(qū)分大小寫
equals() 是嚴格內(nèi)容比較,大小寫不同就不相等。
大小寫敏感比較示例
String input = "YES";
System.out.println("yes".equals(input)); // false
System.out.println("yes".equalsIgnoreCase(input)); // true
如果業(yè)務(wù)規(guī)則允許大小寫不敏感,例如用戶輸入 YES、yes、Yes 都算確認,可以使用 equalsIgnoreCase()。
但它只忽略大小寫,不會自動忽略空格:
System.out.println("yes".equalsIgnoreCase(" yes ")); // false
如果輸入來自用戶,通常還要先考慮是否需要 trim() 去掉首尾空白。
8.3 以為equals()永遠比較內(nèi)容
易錯示例
StringBuilder a = new StringBuilder("abc");
StringBuilder b = new StringBuilder("abc");
System.out.println(a.equals(b)); // false
不是所有類都按內(nèi)容重寫了 equals()。
如果是自己寫的類,要根據(jù)業(yè)務(wù)語義決定是否重寫 equals() 和 hashCode()。
8.4 以為new String("abc")和"abc"一樣
易錯示例
String a = "abc";
String b = new String("abc");
System.out.println(a == b); // false
內(nèi)容一樣,只說明 a.equals(b) 為 true。
是否同一個對象,是另一回事。
8.5 把常量池位置說錯
容易說錯的表達:JDK 8 以后字符串常量池在元空間。
更準確的表達:
- HotSpot JDK 7+ 字符串常量池在堆中。
- JDK 8 移除永久代,使用元空間存放類元數(shù)據(jù)。
這兩個變化有關(guān)聯(lián),但不是同一件事。

總結(jié)
先用一張速查表收束:
| 問題 | 結(jié)論 |
|---|---|
基本類型能不能用 == | 能,比較值 |
字符串內(nèi)容能不能用 == | 不要用,== 比較引用 |
equals() 是否一定比較內(nèi)容 | 不一定,取決于類有沒有重寫 |
String.equals() 比較什么 | 比較字符串內(nèi)容 |
equals() 是否忽略大小寫 | 不忽略,大小寫不敏感時用 equalsIgnoreCase() |
字符串字面量為什么 == 可能為 true | 因為常量池復(fù)用了同一個對象 |
new String("abc") 要注意什么 | 會創(chuàng)建新的 String 對象,不要和字面量引用混為一談 |
intern() 做什么 | 返回字符串常量池中的規(guī)范引用 |
再把本文壓縮成三句話:
- == 對引用類型比較的是“是否同一個對象”。
- String.equals() 比較的是字符串內(nèi)容。
- 字符串常量池會讓某些 == 看起來“剛好正確”,但這不是判斷內(nèi)容的可靠方式。
核心結(jié)論: 寫業(yè)務(wù)代碼時,字符串內(nèi)容比較優(yōu)先使用 equals();只有你明確要判斷兩個引用是否指向同一個對象時,才考慮使用 ==。
排查清單
如果字符串比較結(jié)果和你預(yù)期不一致,可以按這 5 個問題快速自查:
| 自查問題 | 處理方式 |
|---|---|
是不是用 == 比較字符串內(nèi)容了? | 改成 equals() |
是不是和字面量用 == 比較,剛好被常量池“騙”了? | 不依賴 ==,統(tǒng)一按內(nèi)容比較 |
| 有沒有大小寫問題? | 需要忽略大小寫時用 equalsIgnoreCase() |
是不是在比較 StringBuilder / StringBuffer 的內(nèi)容? | 先 toString(),再比較字符串內(nèi)容 |
是不是把 new String(...) 和字面量混用了? | 記住內(nèi)容相同不代表對象相同 |
以上就是一文系統(tǒng)梳理Java中字符串比較的高頻誤區(qū)的詳細內(nèi)容,更多關(guān)于Java字符串比較的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot集成Quartz注入Spring管理的類的方法
本篇文章主要介紹了Spring Boot集成Quartz注入Spring管理的類的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
淺談Java內(nèi)部類——靜態(tài)內(nèi)部類
這篇文章主要介紹了Java靜態(tài)內(nèi)部類的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java內(nèi)部類的相關(guān)知識,感興趣的朋友可以了解下2020-08-08
Spring MVC文件上傳大小和類型限制以及超大文件上傳bug問題
這篇文章主要介紹了Spring MVC文件上傳大小和類型限制以及超大文件上傳bug問題,非常具有實用價值,需要的朋友可以參考下2017-10-10
mybatis 插件: 打印 sql 及其執(zhí)行時間實現(xiàn)方法
下面小編就為大家?guī)硪黄猰ybatis 插件: 打印 sql 及其執(zhí)行時間實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06

