【Java】字符串相等判断:从底层原理到实战避坑
大家好呀!作为一名深耕 Java 后端的开发者,今天想和大家聊聊一个看似基础却极易踩坑的问题 ——字符串相等判断。相信不少同学在刚接触 Java 时,都曾被 == 和 equals() 搞得晕头转向,甚至在实际开发中因为混用导致线上 Bug。这篇文章就结合 JDK 源码和实战案例,把字符串相等判断的底层逻辑和最佳实践讲透,文末还有常见面试题解析哦~
一、核心疑问:== 和 equals() 到底差在哪?
这是字符串比较最基础也最关键的问题,我们直接从本质 + 源码双维度拆解:
1. == 操作符:比较的是「对象地址」
== 是 Java 的原生运算符,它的作用只有一个:判断两个变量是否指向同一个对象(即内存地址是否相同)。
对于基本数据类型(int、char 等),== 比较的是值;但对于引用数据类型(String、自定义对象等),== 比较的是对象在堆内存中的地址。
2. equals() 方法:比较的是「对象内容」(需重写)
equals() 是 Object 类的方法,默认实现其实就是 ==:
// Object类的equals()源码(JDK8/JDK17一致)
public boolean equals(Object obj) {
return (this == obj); // 直接比较地址
}
但 String 类重写了 equals() 方法,核心逻辑是「先判地址,再判类型,最后逐字符比较内容」。我们来看关键源码:
JDK8 String.equals () 源码:
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;
}
JDK17 String.equals () 源码优化:
JDK17 简化了逻辑,且针对不同字符编码(UTF16/Latin1)做了优化,但核心思想不变:
public boolean equals(Object anObject) {
// 地址相同直接返回true
if (this == anObject) {
return true;
}
// 仅处理String类型
if (anObject instanceof String s) {
// 先比较编码和长度,再逐字符对比
return cmp(s.value, s.coder) == 0;
}
return false;
}
二、关键前提:字符串常量池的影响
理解字符串比较,必须先搞懂「字符串常量池」—— 这是 JVM 为了优化字符串创建而设计的缓存机制,也是很多面试题的考点!
1. 字符串创建的两种方式差异:
| 创建方式 | 内存分布 | 是否复用常量池 |
|---|---|---|
String s = "云扬" | 直接从常量池获取(没有则创建) | 是 |
String s = new String("云扬") | 堆内存新建对象,常量池可能同步创建 | 否 |
2. 代码示例:常量池对比较结果的影响
public class StringPoolDemo {
public static void main(String[] args) {
// 方式1:直接赋值,复用常量池
String s1 = "云扬";
String s2 = "云扬";
// 方式2:new关键字,堆内存新建对象
String s3 = new String("云扬");
String s4 = new String("云扬");
// 比较结果分析
System.out.println(s1 == s2); // true(同一常量池对象,地址相同)
System.out.println(s1 == s3); // false(s1在常量池,s3在堆,地址不同)
System.out.println(s3 == s4); // false(两个不同的堆对象,地址不同)
System.out.println(s1.equals(s3)); // true(内容相同)
}
}
三、进阶技巧:intern() 方法的妙用
intern() 是 String 类的 native 方法,作用是「将字符串对象加入常量池(若不存在则创建),并返回常量池中的引用」。
代码示例:intern () 对比较结果的改变
public class InternDemo {
public static void main(String[] args) {
String s1 = new String("云扬"); // 堆对象,常量池已创建"云扬"
String s2 = s1.intern(); // 返回常量池中的"云扬"引用
String s3 = "云扬"; // 直接引用常量池对象
System.out.println(s1 == s3); // false(s1在堆,s3在常量池)
System.out.println(s2 == s3); // true(s2和s3都指向常量池)
}
}
注意:JDK7 后,intern () 不会再复制字符串到永久代,而是直接记录堆对象的引用,优化了内存占用。
四、更安全的比较方案:Objects.equals () 和 contentEquals ()
实际开发中,直接使用 String.equals() 可能遇到空指针问题,而 contentEquals() 支持更多字符序列类型,我们来看这两个实用方法:
1. Objects.equals ():避免空指针的神器
Objects.equals() 是 JDK7 新增的静态方法,底层逻辑:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
代码示例:空指针场景对比
public class SafeEqualsDemo {
public static void main(String[] args) {
String s1 = null;
String s2 = "云扬";
// 直接使用equals():空指针异常!
// System.out.println(s1.equals(s2)); // NullPointerException
// 使用Objects.equals():安全无异常
System.out.println(Objects.equals(s1, s2)); // false
System.out.println(Objects.equals(s2, "云扬")); // true
System.out.println(Objects.equals(null, null)); // true(特殊场景支持)
}
}
2. contentEquals ():支持多字符序列类型
contentEquals() 可以比较 String 与任何实现 CharSequence 接口的对象(如 StringBuffer、StringBuilder),且会处理同步(针对 StringBuffer)。
代码示例:contentEquals () 的灵活用法
public class ContentEqualsDemo {
public static void main(String[] args) {
String s = "云扬的博客";
StringBuilder sb = new StringBuilder("云扬的博客");
StringBuffer sbf = new StringBuffer("云扬的博客");
// 支持StringBuilder/StringBuffer,无需手动转String
System.out.println(s.contentEquals(sb)); // true
System.out.println(s.contentEquals(sbf)); // true
// 对比:equals() 不支持其他类型
System.out.println(s.equals(sb)); // false(类型不同)
}
}
注意:contentEquals () 会先判断长度,再逐字符比较,性能略低于 equals (),非必要场景无需使用。
五、常见面试题解析(含代码验证)
结合前面的知识点,我们来拆解几道高频面试题:
| 题目 | 结果 | 核心原因 |
|---|---|---|
"云扬" == "云" + "扬" | true | 编译期优化:”云”+”扬” 直接拼接为 “云扬”,与常量池对象地址相同 |
"云扬" == new String("云扬") | false | 前者在常量池,后者在堆,地址不同 |
new String("云扬").equals("云扬") | true | equals () 比较内容,与内存地址无关 |
new String("云扬").intern() == "云扬" | true | intern () 返回常量池引用,与直接赋值的对象地址相同 |
代码验证:
public class InterviewDemo {
public static void main(String[] args) {
System.out.println("云扬" == "云" + "扬"); // true(编译期优化)
System.out.println("云扬" == new String("云扬")); // false(地址不同)
System.out.println(new String("云扬").equals("云扬")); // true(内容相同)
System.out.println(new String("云扬").intern() == "云扬"); // true(intern()作用)
}
}
六、最佳实践总结
- 比较字符串内容:优先使用
Objects.equals(s1, s2)(避免空指针); - 确定非空场景:可直接用
s1.equals(s2)(性能略优); - 比较 String 与 StringBuilder/StringBuffer:用
contentEquals(); - 绝对不要用
==比较字符串内容(仅用于判断是否为同一对象); - 字符串创建:优先用
String s = "xxx"(复用常量池,节省内存),避免频繁用new String()。
以上就是字符串相等判断的全部核心知识点啦!如果大家在实际开发中遇到相关问题,或者有其他想深入了解的 Java 技术点,欢迎在评论区留言交流~



