核心结论先行
在 Java 中,使用 比较两个 String 对象时,比较的不是它们的内容是否相同,而是它们在内存中的地址(引用)是否相同。

s1 == s2返回true:说明s1和s2指向的是同一个内存对象。s1 == s2返回false:说明s1和s2指向的是两个不同的内存对象,即使它们的内容完全一样。
要比较 String 对象的是否相等,必须使用 .equals() 方法。
比较的是什么?
是一个关系运算符,用于比较两个基本数据类型的值是否相等,或者两个对象的引用地址是否指向同一个内存位置。
- 基本数据类型 (如
int,double,char,boolean): 比较的是它们的值。 - 引用数据类型 (如
String,Integer, 自定义类): 比较的是它们的内存地址(引用)。
String 是一个引用数据类型,s1 == s2 实际上是在问:“变量 s1 中存储的内存地址”和“变量 s2 中存储的内存地址”是不是同一个?
.equals() 比较的是什么?
.equals() 是 Object 类中的一个方法,其默认行为和 一样,也是比较对象的引用地址。

String 类重写(Override)了 .equals() 方法,使其专门用于比较两个 String 对象的内容(字符序列)是否相同,而不管它们是不是同一个对象。
为什么会有 String 池(String Pool)?
要完全理解 String 的 比较,就必须了解 Java 的 字符串常量池。
-
目的:为了提高性能和减少内存占用,Java 会缓存(池化)一些
String字面量,当你创建一个字符串字面量时,JVM 会先在字符串池中查找是否存在相同内容的字符串,如果存在,就直接返回池中对象的引用;如果不存在,就创建一个新的字符串对象并存入池中,然后返回其引用。 -
两种创建
String的方式:
(图片来源网络,侵删)- 字面量赋值:
String s = "hello"; new关键字创建:String s = new String("hello");
- 字面量赋值:
这两种方式的内存分配机制完全不同,这也导致了 比较结果的差异。
实例分析
让我们通过几个经典的例子来彻底搞懂。
示例 1:字面量赋值,内容相同
String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); // 输出: true System.out.println(s1.equals(s2)); // 输出: true
解释:
- 当执行
String s1 = "hello";时,JVM 在字符串池中查找 "hello",发现没有,于是创建一个 "hello" 对象,并将其引用赋给s1。 - 当执行
String s2 = "hello";时,JVM 再次在字符串池中查找 "hello",这次找到了!于是它不会创建新对象,而是直接将池中已存在的 "hello" 对象的引用赋给s2。 s1和s2指向的是池中同一个对象,它们的引用地址相同,s1 == s2为true自然也相同,s1.equals(s2)也为true。
示例 2:new 关键字创建,内容相同
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // 输出: false
System.out.println(s1.equals(s2)); // 输出: true
解释:
new关键字强制在堆内存中创建一个新的对象,它不会检查字符串池。String s1 = new String("hello");:JVM 会在字符串池中创建一个 "hello"(如果还没有的话),然后在堆内存中再创建一个新的 "hello" 对象,并将堆中对象的引用赋给s1。String s2 = new String("hello");:同样,JVM 会在字符串池中找到 "hello"(如果示例1没运行过,它会创建;如果运行过,它就直接用),然后在堆内存中再创建一个新的 "hello" 对象,并将这个新对象的引用赋给s2。- 结果是,
s1和s2分别指向堆内存中两个不同的 "hello" 对象,它们的引用地址不同,s1 == s2为false,但由于它们的内容都是 "hello",s1.equals(s2)为true。
示例 3:一个字面量,一个 new相同
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1 == s2); // 输出: false
System.out.println(s1.equals(s2)); // 输出: true
解释:
s1指向字符串池中的 "hello" 对象。s2指向堆内存中新创建的 "hello" 对象。- 一个在池,一个在堆,引用地址必然不同,
s1 == s2为false相同,s1.equals(s2)为true。
示例 4:字符串拼接
String s1 = "hello"; String s2 = "he" + "llo"; // 编译器优化后,等同于 s2 = "hello"; System.out.println(s1 == s2); // 输出: true System.out.println(s1.equals(s2)); // 输出: true
解释:
- Java 编译器非常智能,它会在编译期间对字符串常量的拼接进行优化。
"he" + "llo"会在编译时直接合并成"hello"。 - 这实际上退化成了和示例1一样的情况,
s2也是直接从字符串池中获取的 "hello" 的引用。
String s1 = "hello"; String s2 = "he"; String s3 = s2 + "llo"; // 运行时拼接 System.out.println(s1 == s3); // 输出: false System.out.println(s1.equals(s3)); // 输出: true
解释:
- 这次拼接发生在运行时,因为
s2是一个变量,编译器无法在编译期确定其值。 s2 + "llo"的实际执行过程是:new StringBuilder().append(s2).append("llo").toString()。toString()方法会new一个新的String对象放在堆内存中。s3指向的是堆内存中的新对象,而s1指向的是池中的对象,s1 == s3为false。
总结与最佳实践
| 比较场景 | s1 == s2 |
s1.equals(s2) |
原因 |
|---|---|---|---|
| 两个字面量,内容相同 | true |
true |
引用指向字符串池中的同一个对象。 |
两个 new 创建,内容相同 |
false |
true |
引用指向堆内存中两个不同的对象。 |
一个字面量,一个 new相同 |
false |
true |
一个指向池,一个指向堆。 |
| 运行时变量拼接 | false |
true |
操作会创建新的 String 对象在堆上。 |
黄金法则
只要你想比较
String的内容,就永远使用.equals()方法。只在一种情况下可以安全地用于
String:当你明确地想判断两个String变量是否指向同一个对象时(为了性能优化,避免重复创建相同的字符串),但在绝大多数业务逻辑中,这都不是你的目的。
equals() 的潜在问题:NullPointerException
s1.equals(s2) 有一个小坑:s1 是 null,调用 s1.equals(s2) 会抛出 NullPointerException。
为了解决这个问题,更安全的写法是:
// 推荐:将常量放在前面 "hello".equals(s1); // s1 是 null,这里返回 false,不会报错 // 或者使用 Objects.equals() (Java 7+) import java.util.Objects; Objects.equals(s1, s2); // 内部做了 null 检查,非常安全
intern() 方法补充
String 还有一个 intern() 方法,它可以显式地将一个字符串(无论是来自堆还是字面量)“推入”字符串池中,并返回池中字符串的引用。
String s1 = new String("hello");
String s2 = s1.intern(); // 将 s1 的内容 "hello" 放入池中,如果池中没有就放,然后返回池中的引用
String s3 = "hello";
System.out.println(s1 == s2); // false, s1 仍在堆,s2 指向池
System.out.println(s2 == s3); // true, s2 和 s3 都指向池中的同一个对象
intern() 方法在处理大量重复且较长的字符串时,可以节省大量内存,但使用时需谨慎,因为它会增加一些性能开销。
