杰瑞科技汇

Java字符串比较用==还是equals?

核心结论先行

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

Java字符串比较用==还是equals?-图1
(图片来源网络,侵删)
  • s1 == s2 返回 true:说明 s1s2 指向的是同一个内存对象
  • s1 == s2 返回 false:说明 s1s2 指向的是两个不同的内存对象,即使它们的内容完全一样。

要比较 String 对象的是否相等,必须使用 .equals() 方法。


比较的是什么?

是一个关系运算符,用于比较两个基本数据类型的值是否相等,或者两个对象的引用地址是否指向同一个内存位置。

  • 基本数据类型 (如 int, double, char, boolean): 比较的是它们的
  • 引用数据类型 (如 String, Integer, 自定义类): 比较的是它们的内存地址(引用)

String 是一个引用数据类型,s1 == s2 实际上是在问:“变量 s1 中存储的内存地址”和“变量 s2 中存储的内存地址”是不是同一个?


.equals() 比较的是什么?

.equals()Object 类中的一个方法,其默认行为和 一样,也是比较对象的引用地址。

Java字符串比较用==还是equals?-图2
(图片来源网络,侵删)

String重写(Override).equals() 方法,使其专门用于比较两个 String 对象的内容(字符序列)是否相同,而不管它们是不是同一个对象。


为什么会有 String 池(String Pool)?

要完全理解 String 的 比较,就必须了解 Java 的 字符串常量池

  1. 目的:为了提高性能和减少内存占用,Java 会缓存(池化)一些 String 字面量,当你创建一个字符串字面量时,JVM 会先在字符串池中查找是否存在相同内容的字符串,如果存在,就直接返回池中对象的引用;如果不存在,就创建一个新的字符串对象并存入池中,然后返回其引用。

  2. 两种创建 String 的方式

    Java字符串比较用==还是equals?-图3
    (图片来源网络,侵删)
    • 字面量赋值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
  • s1s2 指向的是池中同一个对象,它们的引用地址相同,s1 == s2true自然也相同,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
  • 结果是,s1s2 分别指向堆内存中两个不同的 "hello" 对象,它们的引用地址不同,s1 == s2false,但由于它们的内容都是 "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 == s2false相同,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 == s3false

总结与最佳实践

比较场景 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) 有一个小坑:s1null,调用 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() 方法在处理大量重复且较长的字符串时,可以节省大量内存,但使用时需谨慎,因为它会增加一些性能开销。

分享:
扫描分享到社交APP
上一篇
下一篇