【Java】字符串相等判断:从源码到实战,避坑指南
大家好,我是云扬~ 实际开发中,字符串相等判断是高频场景,但不少同学会在 == 和 equals() 之间踩坑,甚至忽略更优雅的进阶方案。今天就结合源码和实战场景,彻底搞懂 Java 字符串相等的判断逻辑!
一、核心疑问:== 和 equals() 到底差在哪?
1.1 本质区别
==:比较对象内存地址(判断是否是同一个对象)equals():比较对象内容(需子类重写,否则等价于==)
1.2 根源:Object 类的默认实现
Java 中所有类都继承自 Object,其 equals() 源码如下:
// Object 类的 equals() 源码
public boolean equals(Object obj) {
return (this == obj); // 直接用 == 比较地址
}
如果子类(比如 String)不重写 equals(),调用时本质还是比较地址 —— 这也是很多新手踩坑的核心原因!
二、String 类的 equals():源码深度解析
String 重写了 equals() 方法,专门用于比较字符串内容,JDK8 和 JDK17 实现略有差异,但核心逻辑一致。
2.1 JDK8 版本源码
public boolean equals(Object anObject) {
// 1. 先判断是否是同一个对象(地址相等),是则直接返回 true
if (this == anObject) {
return true;
}
// 2. 判断参数是否是 String 类型,不是则返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 3. 长度不同直接返回 false(优化性能)
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 4. 逐个字符比较
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
2.2 JDK17 版本源码(简化版)
public boolean equals(Object anObject) {
// 1. 地址相等直接返回 true(同 JDK8)
if (this == anObject) {
return true;
}
// 2. 优化类型判断:直接用 getClass() 而非 instanceof(避免子类冒充)
if (anObject instanceof String s) {
// 3. 按字符编码比较(支持 UTF16/Latin1,逻辑更灵活)
return compareTo(s) == 0;
}
return false;
}
核心优化:JDK17 用 compareTo() 统一字符比较逻辑,同时通过 instanceof String s 简化类型转换,性能和可读性更优。
三、字符串常量池:为什么不推荐 new String()?
要彻底理解判断逻辑,必须先搞懂 字符串常量池(String Constant Pool):
- 直接赋值:
String s = "云扬"→ 优先从常量池获取对象,若不存在则创建并缓存 - new 关键字:
String s = new String("云扬")→ 必然在堆内存创建新对象,同时常量池会同步创建该字符串(若未存在)
示意图:
常量池:["云扬"]
堆内存:new String("云扬") → 新地址
这就是为什么 new String("云扬") == "云扬" 会返回 false—— 两者内存地址完全不同!
四、实战例题:6 种场景全覆盖
直接上代码,结合注释理解:
public class StringEqualsTest {
public static void main(String[] args) {
// 1. new String 与常量池字符串比较(内容相同,地址不同)
System.out.println(new String("云扬").equals("云扬")); // true(equals 比内容)
// 2. new String 与常量池字符串用 == 比较
System.out.println(new String("云扬") == "云扬"); // false(地址不同)
// 3. 两个 new String 比较(堆中两个不同对象)
System.out.println(new String("云扬") == new String("云扬")); // false(地址不同)
// 4. 常量池字符串直接比较(同一对象)
System.out.println("云扬" == "云扬"); // true(常量池缓存复用)
// 5. 常量字符串拼接(编译器优化为完整字符串)
System.out.println("云扬" == "云" + "扬"); // true(编译后为 "云扬")
// 6. intern() 方法:将堆中字符串入池并返回常量池引用
System.out.println(new String("云扬").intern() == "云扬"); // true(intern() 返回常量池对象)
}
}

运行结果:true false false true true true,完全符合预期!
五、进阶方案:更优雅的字符串比较
除了 String.equals(),还有两个场景化更强的方案,避免踩坑同时提升代码健壮性。
5.1 Objects.equals ():避免空指针异常
直接用 a.equals(b) 时,若 a 为 null 会抛出 NullPointerException,而 Objects.equals() 完美解决这个问题:
import java.util.Objects;
public class ObjectsEqualsTest {
public static void main(String[] args) {
String a = null;
String b = "云扬";
// 风险:a 为 null 时抛出空指针
// System.out.println(a.equals(b));
// 安全:无需判空,直接比较
System.out.println(Objects.equals(a, b)); // false
System.out.println(Objects.equals("云扬", b)); // true
}
}

其源码逻辑(简单高效):
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
5.2 String.contentEquals ():兼容所有字符序列
如果需要比较 String 与 StringBuffer、StringBuilder 等 CharSequence 实现类,contentEquals() 是最优选择(自动处理同步和类型适配):
public class ContentEqualsTest {
public static void main(String[] args) {
String s = "云扬";
StringBuffer sb = new StringBuffer("云扬");
StringBuilder sbd = new StringBuilder("云扬");
// 支持 StringBuffer(自动同步)
System.out.println(s.contentEquals(sb)); // true
// 支持 StringBuilder
System.out.println(s.contentEquals(sbd)); // true
// 支持 String(等价于 equals())
System.out.println(s.contentEquals("云扬")); // true
}
}

JDK17 源码优化:对 StringBuffer 自动加锁同步,避免线程安全问题,无需手动处理。
六、核心总结:如何选择比较方式?
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 两个 String 比较内容 | Objects.equals(a, b) | 避免空指针,最常用 |
| 比较 String 与 CharSequence | contentEquals() | 兼容 StringBuffer/StringBuilder |
| 判断是否是同一个字符串对象 | == | 仅用于常量池复用场景(谨慎使用) |
最后记住:字符串比较,优先用 Objects.equals(),除非明确需要判断对象身份(地址),否则坚决不用 ==!
以上就是字符串相等判断的完整知识点啦~ 实际开发中你还遇到过哪些相关坑?或者有其他场景想补充?欢迎在评论区留言讨论,我会第一时间回复!如果需要获取文中完整代码示例,也可以私信我哦~



