== 与 equals:到底比较什么?
在写代码的时候,你有没有被这个问题坑过?
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // 输出什么?
System.out.println(a.equals(b)); // 输出什么?
很多人会说一个是 true,一个是 false。但如果我追问:"为什么 equals 不是 == ?"很多人就答不上来了。
这个问题是 Java 基础中的基础,但恰恰是最容易被忽略的细节。面试中经常会延伸问到 String 的 intern 方法、hashCode 的重要性等等。今天我们就来把这个知识点彻底讲透。
一、真实面试场景
候选人小王在面试字节跳动的时候,被问到这道题:
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // 问:输出什么?
System.out.println(s1 == s3); // 问:输出什么?
System.out.println(s1.equals(s3)); // 问:输出什么?
小王回答:"s1 == s2 是 true,s1 == s3 是 false,s1.equals(s3) 是 true。"
面试官点点头,继续追问:"那为什么 s1 == s2 是 true,但 s1 == s3 是 false?"
小王说:"因为 s3 是 new 出来的,在堆里创建了新对象..."
面试官又问:"那 equals 呢?为什么 s1.equals(s3) 是 true?String 的 equals 是怎么实现的?"
小王开始支支吾吾。
【面试官心理】
这个问题看起来是考 == 和 equals 的区别,实际上我想知道的是:候选人有没有理解 String 的不可变性、字符串常量池、equals 的重写逻辑,以及 hashCode 和 equals 的关系。任何一个问题延伸追问下去,都能暴露他到底是真的理解还是只是"知道结论"。
二、== 的本质:比较的是什么?
2.1 基本类型:比较值
对于基本类型,== 比较的是值本身:
int a = 10;
int b = 10;
System.out.println(a == b); // true,值相等
double d1 = 0.1;
double d2 = 0.1;
System.out.println(d1 == d2); // true,值相等
char c1 = 'A';
char c2 = 65; // 'A' 的 ASCII 码是 65
System.out.println(c1 == c2); // true,字符比较的是 Unicode 值
2.2 引用类型:比较地址(引用)
对于引用类型,== 比较的是内存地址,也就是"是不是同一个对象":
Person p1 = new Person("张三");
Person p2 = new Person("张三");
Person p3 = p1; // p3 和 p1 指向同一个对象
System.out.println(p1 == p2); // false,不同对象,地址不同
System.out.println(p1 == p3); // true,p1 和 p3 是同一个对象
2.3 ❌ 常见错误:认为 == 比较的是内容
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false!不是比较内容,是比较地址
很多人会误以为 s1 == s2 应该输出 true,因为内容相同。但实际上 == 比较的是地址,不是内容。两次 new String() 创建了两个不同的对象,地址不同,所以是 false。
⚠️
记住一个原则:== 永远比较的是"是不是同一个东西",而不是"内容是否相等"。基本类型比较值,引用类型比较地址。
三、equals 的本质:可重写的比较方法
3.1 Object 类的 equals 默认实现
在 Object 类中,equals 方法的默认实现就是 ==:
public class Object {
public boolean equals(Object obj) {
return (this == obj); // 调用的就是 ==
}
}
所以如果一个类没有重写 equals,那么 equals 和 == 效果一样——比较地址。
3.2 为什么 String 要重写 equals?
因为 String 的业务场景需要比较"内容"而不是"地址":
String s1 = new String("hello");
String s2 = new String("hello");
// 没有重写 equals 的话,下面的结果应该是 false
System.out.println(s1.equals(s2)); // true,因为 String 重写了 equals
3.3 String equals 的实现原理
public boolean equals(Object anObject) {
// 1. 先比较地址,如果是同一个对象直接返回 true
if (this == anObject) {
return true;
}
// 2. 再比较内容
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
// 比较字符数组的每一个字符
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
这个实现做了两件事:
- 快速判断:先比较地址,同一个对象直接返回 true
- 内容比较:再逐字符比较字符数组的内容
3.4 【直观类比】equals 的重写逻辑
想象你在图书馆找书:
- == 比较:比较的是"是不是同一本书"(同一个物理位置)
- equals 比较:比较的是"内容是否相同"(同一个ISBN的书,虽然不是同一本纸,但内容一样)
String 重写 equals,就是让它按照 ISBN 来比较,而不是按照"物理位置"。
四、equals 的契约:不得不注意的五个要点
如果你要重写 equals 方法,必须遵守 Object 规范中定义的五个契约:
4.1 自反性(Reflexive)
x.equals(x) == true // 任何对象都等于自己
4.2 对称性(Symmetric)
x.equals(y) == y.equals(x) // x和y的比较结果应该一致
错误示例:
// 错误的equals实现
class Person {
String name;
int age;
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
Person p = (Person) obj;
return this.name.equals(p.name) && this.age == p.age;
}
// 错误!当 obj 是字符串时会和 Person 比较不对称
if (obj instanceof String) {
return obj.equals(this.name);
}
return false;
}
}
4.3 传递性(Transitive)
if (x.equals(y) && y.equals(z)) {
x.equals(z) == true // x也应该等于z
}
4.4 一致性(Consistent)
// 多次调用应该返回相同的结果(前提是对象没有被修改)
x.equals(y) == x.equals(y) // 结果应该一致
4.5 非空性(Non-null)
x.equals(null) == false // 任何对象都不等于 null
五、hashCode:与 equals 必须配对使用
5.1 为什么重写 equals 必须重写 hashCode?
这是 Object 规范中的一个重要约定:相等的对象必须有相等的 hashCode。
class Person {
String name;
int age;
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
Person p = (Person) obj;
return this.name.equals(p.name) && this.age == p.age;
}
return false;
}
// 如果不重写 hashCode,这里就会违反约定
}
不重写 hashCode 会导致什么问题?看看 HashMap 的场景:
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25);
map.put(p1, "工程师");
String result = map.get(p2); // 结果可能是 null!
如果只重写 equals 不重写 hashCode,p1 和 p2 的 hashCode 可能不同,导致 HashMap 把它们存在不同的桶里,get 的时候找不到。
⚠️
这是生产环境中一个非常容易踩的坑:两个对象 equals 为 true,但 hashCode 不相等。HashMap、HashSet 等基于哈希的集合会出问题,具体表现为"存进去但取不出来"。排查起来很隐蔽,因为两个对象的逻辑是"相等"的,但存储和查询的结果不一致。
5.2 hashCode 的实现原则
// 良好的 hashCode 实现示例
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
hashCode 的实现原则:
- 一致性:同一个对象在不被修改的情况下,多次调用 hashCode 应该返回相同的值
- 效率:计算应该快
- 分布均匀:不同的对象尽量有不同的 hashCode,使哈希表分布均匀
六、常见面试题解析
6.1 String 的 == vs equals
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true
为什么?因为:
s1 和 s2 指向字符串常量池中的同一个对象(编译器优化,"hello" 被放在常量池)
s3 是 new 出来的,在堆中创建了新对象,和常量池中的不是同一个
- String 重写了 equals,比较的是内容,所以 s1.equals(s3) 是 true
6.2 Integer 的自动装箱陷阱
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true?
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false?
这个结果取决于 Integer 的缓存机制:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer 有缓存 -128 到 127 的 Integer 对象。所以:
a == b:都在缓存范围内,返回同一个对象,true
c == d:超过缓存范围,每次都 new 新对象,不同地址,false
💡
Integer、Short、Byte、Long、Character 都有类似的缓存机制,只是缓存范围不同。Boolean 直接只有两个常量:Boolean.TRUE 和 Boolean.FALSE。这是一个高频面试点。
6.3 包装类型与 == 的坑
Integer a = 1;
Integer b = 1;
System.out.println(a == b); // true(缓存范围内)
Integer c = new Integer(1);
Integer d = new Integer(1);
System.out.println(c == d); // false(new 的一定是新对象)
System.out.println(a == c); // false(一个在缓存,一个new)
记住:new 出来的对象一定不在缓存里,== 比较的一定是不同的对象。
七、生产场景与避坑
7.1 ❌ 错误示范:使用 == 比较字符串内容
String input = getUserInput();
if (input == "admin") { // 错误!可能永远不等于
// 登录逻辑
}
正确做法:
String input = getUserInput();
if ("admin".equals(input)) { // 正确:使用 equals
// 登录逻辑
}
注意这里把字面量放前面,这样做有两个好处:
- 避免 input 为 null 时抛 NullPointerException
- 直接使用 String 的 equals 比较内容
7.2 ❌ 错误示范:重写 equals 不重写 hashCode
class Order {
private String orderId;
private int amount;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return amount == order.amount && Objects.equals(orderId, order.orderId);
}
// 忘记重写 hashCode,导致 HashSet/HashMap 行为异常
}
正确做法:
class Order {
private String orderId;
private int amount;
@Override
public boolean equals(Object o) {
// ...
}
@Override
public int hashCode() {
return Objects.hash(orderId, amount);
}
}
7.3 ✅ 正确示范:使用 Objects.equals 和 Objects.hash
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof Order &&
Objects.equals(orderId, ((Order) o).orderId) &&
amount == ((Order) o).amount;
}
@Override
public int hashCode() {
return Objects.hash(orderId, amount);
}
使用 Objects 工具类可以让代码更简洁,同时正确处理 null 比较。
八、面试追问链
第一层:基础概念
面试官问:"== 和 equals 的区别是什么?"
标准回答:== 对于基本类型比较值,对于引用类型比较地址(是否是同一个对象)。equals 是 Object 的方法,默认实现和 == 一样,但可以被重写来比较内容。String 重写了 equals,所以可以比较内容。
第二层:实现原理
面试官追问:"String 的 equals 是怎么实现的?"
需要说明:先比较地址(快速判断),再逐字符比较字符数组。涉及到 instanceof 判断、长度比较、字符数组遍历。
第三层:hashCode 关系
面试官追问:"重写 equals 为什么要重写 hashCode?"
需要说明:基于哈希的集合(HashMap、HashSet)在查询时先比较 hashCode 再比较 equals。如果两个对象 equals 为 true 但 hashCode 不同,会导致这些集合无法正常工作。
第四层:实际应用
面试官追问:"你在项目中怎么用 equals?有没有踩过坑?"
可以举实际例子:比如在 Service 层比较业务对象时使用 equals,在使用 HashMap 做缓存时注意重写 hashCode 等等。
【面试官心理】
这个问题的追问空间很大。我可以从 == vs equals 延伸到 String 特性、Integer 缓存、hashCode 契约等等。真正理解这些知识的候选人,能从最基础的 == 讲到哈希表的工作原理,中间没有任何卡顿。
【学习小结】
==:基本类型比较值,引用类型比较地址
equals:默认等价于 ==,可以被重写来比较内容
- String 重写了 equals,所以可以比较内容
- 重写 equals 必须同时重写 hashCode(哈希表契约)
- 使用
"constant".equals(variable) 可以避免空指针