Java面试-== 和 equals() 方法的区别与实现原理
👋 欢迎阅读《Java面试200问》系列博客!
🚀大家好,我是Jinkxs,一名热爱Java、深耕技术一线的开发者。在准备和参与了数十场Java面试后,我深知面试不仅是对知识的考察,更是对理解深度与表达能力的综合检验。
✨本系列将带你系统梳理Java核心技术中的高频面试题,从源码原理到实际应用,从常见陷阱到大厂真题,每一篇文章都力求深入浅出、图文并茂,帮助你在求职路上少走弯路,稳拿Offer!
🔍今天我们要聊的是:《== 和 equals() 方法的区别与实现原理》。准备好了吗?Let’s go!
🎯 引言:Java 世界的“身份证”与“长相”之争
“在Java的世界里,最像‘认人’的,不是人脸识别系统,而是——
==
和equals()
。”
想象一下:
- 你去银行办业务,柜员问:“你是张三吗?”
- 你回答:“是的!”
- 柜员拿出身份证一刷:“证件号:123456” → 这是
==
,看是不是同一个人(同一个对象)。 - 柜员再看看你的脸:“嗯,长得确实像张三” → 这是
equals()
,看是不是长得一样(内容相等)。
但问题来了:
- 如果你是张三的双胞胎弟弟,脸长得一模一样,但身份证不同,柜员认不认?
- 如果你是张三,但整容了,脸变了,身份证还是那个,柜员还认你吗?
今天,我们就来扒一扒这两位“认人专家”——==
和 equals()
——的底细,看看它们如何在Java世界里“认人”。
📚 目录导航(别走丢了)
==
:身份证号比对,看是不是“同一个人”equals()
:长相比对,看是不是“长得一样”==
和equals()
的默认行为揭秘- 为什么
String
的==
和equals()
有时一样,有时不一样? - 自定义类的
equals()
正确写法(含10个坑) hashCode()
与equals()
的“黄金搭档”关系- 面试常见陷阱大揭秘
- 最佳实践与使用场景总结
1. ==
:身份证号比对,看是不是“同一个人”
==
是Java中的相等运算符,它比较的是两个变量的值。
但这个“值”是什么,取决于变量的类型:
- 对于基本类型:比较的是实际的数值。
- 对于引用类型:比较的是对象的内存地址(引用)。
✅ 基本类型:数值比对
int a = 100;
int b = 100;
System.out.println(a == b); // ✅ true,数值相等double x = 3.14;
double y = 3.14;
System.out.println(x == y); // ✅ true
✅ 基本类型直接比“值”,简单粗暴。
✅ 引用类型:引用(内存地址)比对
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // ❌ false!
❓ 为什么?虽然内容都是 “Hello”,但
s1
和s2
是两个不同的对象,内存地址不同。
String s3 = s1;
System.out.println(s1 == s3); // ✅ true,s3 和 s1 指向同一个对象
✅
==
只关心“是不是同一个对象”,不关心“内容是否一样”。
📊 ==
比较机制图解
s1 → [对象1: "Hello"]
s2 → [对象2: "Hello"]
s3 → s1 → [对象1: "Hello"]s1 == s2 → ❌ 地址不同
s1 == s3 → ✅ 地址相同
✅ 记住:
==
看的是“身份证号”,不是“长相”。
2. equals()
:长相比对,看是不是“长得一样”
equals()
是 Object
类的一个方法,用于比较两个对象的“逻辑相等性”。
✅ 默认行为:等价于 ==
Object
类中的 equals()
方法默认实现就是用 ==
比较引用:
public boolean equals(Object obj) {return (this == obj);
}
所以,对于没有重写 equals()
的类,equals()
和 ==
效果一样。
✅ 重写 equals()
:自定义“长相”标准
但很多类(如 String
、Integer
、List
)都重写了 equals()
,让它比较内容而不是引用。
示例:String
的 equals()
String s1 = new String("Java");
String s2 = new String("Java");System.out.println(s1 == s2); // ❌ false,不同对象
System.out.println(s1.equals(s2)); // ✅ true,内容相同
✅
String
的equals()
比较的是字符序列是否相同。
3. ==
和 equals()
的默认行为揭秘
✅ 代码示例:未重写 equals()
的类
class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}
}public class DefaultEquals {public static void main(String[] args) {Person p1 = new Person("张三", 25);Person p2 = new Person("张三", 25);System.out.println(p1 == p2); // ❌ false,不同对象System.out.println(p1.equals(p2)); // ❌ false,因为 equals() 默认用 == 比较}
}
❓ 问题来了:两个
Person
对象,名字年龄都一样,按理说“长得一样”,但equals()
却说不相等!
原因:Person
类没有重写 equals()
,用的是 Object
的默认实现,也就是 ==
。
4. 为什么 String
的 ==
和 equals()
有时一样,有时不一样?
这是面试必问神题!
✅ 代码示例:字符串常量池的“魔法”
String a = "Hello";
String b = "Hello";
System.out.println(a == b); // ✅ true
System.out.println(a.equals(b)); // ✅ trueString c = new String("Hello");
String d = new String("Hello");
System.out.println(c == d); // ❌ false
System.out.println(c.equals(d)); // ✅ true
❓ 为什么
a == b
是true
?它们不是两个变量吗?
🔍 答案:字符串常量池(String Pool)!
当你用双引号创建字符串时,JVM会先检查字符串常量池:
- 如果池中已有相同内容的字符串,就返回同一个引用。
- 如果没有,就创建新字符串并放入池中。
所以:
String a = "Hello"; // 池中无,创建 "Hello" 并放入池,a 指向它
String b = "Hello"; // 池中有,b 直接指向同一个 "Hello"
a == b → true // 同一个对象
而 new String("Hello")
会强制创建新对象,不管池中有没有。
✅ 记住:
==
比较引用,equals()
比较内容。
字符串常量池让==
有时“看起来”像在比较内容,但本质没变。
📊 字符串常量池机制图解
字符串常量池:"Hello" → [内存地址 0x100]a = "Hello" → 指向 0x100
b = "Hello" → 指向 0x100
a == b → ✅ truec = new String("Hello") → 在堆上创建新对象 [0x200],内容 "Hello"
d = new String("Hello") → 在堆上创建新对象 [0x300],内容 "Hello"
c == d → ❌ false
5. 自定义类的 equals()
正确写法(含10个坑)
想让你的类能正确比较“长相”,必须重写 equals()
。
✅ 正确写法模板
@Override
public boolean equals(Object obj) {// 1. 自反性:自己和自己比较if (this == obj) return true;// 2. 空值检查if (obj == null) return false;// 3. 类型检查if (getClass() != obj.getClass()) return false;// 或者用 instanceof(见坑4)// 4. 类型转换Person other = (Person) obj;// 5. 逐个字段比较return age == other.age && Objects.equals(name, other.name);// 用 Objects.equals() 安全处理 null
}
❌ 坑1:忘记 @Override
注解
// ❌ 错误:方法签名写错了,其实是新方法,不是重写
public boolean equals(Person p) {return this.name.equals(p.name) && this.age == p.age;
}// ✅ 正确:加上 @Override,让编译器帮你检查
@Override
public boolean equals(Object obj) { ... }
✅
@Override
能防止你写错方法名或参数类型。
❌ 坑2:不检查 null
@Override
public boolean equals(Object obj) {Person other = (Person) obj;return name.equals(other.name) && age == other.age;// ❌ 如果 obj 为 null,这里抛 NPE!
}
✅ 必须先检查
obj == null
。
❌ 坑3:用 instanceof
不当,破坏对称性
class Person {String name;int age;// ...@Overridepublic boolean equals(Object obj) {if (obj instanceof Person) {Person p = (Person) obj;return name.equals(p.name) && age == p.age;}return false;}
}class Student extends Person {String studentId;// ...@Overridepublic boolean equals(Object obj) {if (obj instanceof Student) {Student s = (Student) obj;return super.equals(obj) && studentId.equals(s.studentId);}return false;}
}
❓ 问题:
Person p = new Person("张三", 25);
Student s = new Student("张三", 25, "S001");
p.equals(s)
→true
(因为 s 是 Person)
s.equals(p)
→false
(因为 p 不是 Student)
❌ 不对称!
✅ 解决方案:用
getClass()
比较,确保类型完全相同。
❌ 坑4:浮点数比较用 ==
class Point {double x, y;// ...@Overridepublic boolean equals(Object obj) {Point p = (Point) obj;return x == p.x && y == p.y; // ❌ 浮点数精度问题!}
}
✅ 正确做法:用
Double.compare()
或允许误差。
return Double.compare(x, p.x) == 0 && Double.compare(y, p.y) == 0;
// 或
return Math.abs(x - p.x) < 1e-9 && Math.abs(y - p.y) < 1e-9;
❌ 坑5:数组字段比较不当
class Data {int[] values;// ...@Overridepublic boolean equals(Object obj) {Data d = (Data) obj;return Arrays.equals(values, d.values); // ✅ 正确// return values == d.values; // ❌ 错误,比较引用}
}
✅ 用
Arrays.equals()
比较数组内容。
❌ 坑6:集合字段比较不当
class Group {List<String> members;// ...@Overridepublic boolean equals(Object obj) {Group g = (Group) obj;return members.equals(g.members); // ✅ 正确,List 重写了 equals()}
}
✅ 大多数集合类都正确重写了
equals()
。
❌ 坑7:不满足“传递性”
// ❌ 错误示例
if (name != null ? name.equals(other.name) : other.name == null) {// 只比较了 name,忽略了 agereturn true;
}
return false;
✅ 必须比较所有相关字段,确保
a.equals(b)
且b.equals(c)
蕴含a.equals(c)
。
❌ 坑8:可变字段作为 equals 依据
class MutablePerson {private String name; // 可变@Overridepublic boolean equals(Object obj) {MutablePerson p = (MutablePerson) obj;return name.equals(p.name);}
}
❓ 问题:如果
name
被修改,equals()
结果会变,可能导致HashSet
中的对象“丢失”。
✅ 建议:用不可变字段(如
final
)作为equals()
依据。
❌ 坑9:性能问题——不必要的复杂计算
@Override
public boolean equals(Object obj) {// ❌ 错误:每次比较都计算哈希值return this.hashCode() == obj.hashCode();
}
✅
equals()
应该快速、简单。复杂计算放hashCode()
。
❌ 坑10:忘记 hashCode()
的约定
这是最大的坑!见下一节。
6. hashCode()
与 equals()
的“黄金搭档”关系
hashCode()
和 equals()
是一对“黄金搭档”,必须同时重写,且遵守一个铁律:
✅ 如果两个对象用
equals()
比较相等,那么它们的hashCode()
必须相等。
✅ 为什么?
因为 HashSet
、HashMap
等集合依赖 hashCode()
来快速定位对象。
示例:不重写 hashCode()
的灾难
class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Person p = (Person) obj;return age == p.age && Objects.equals(name, p.name);}// ❌ 忘记重写 hashCode()!
}public class HashCodeProblem {public static void main(String[] args) {Person p1 = new Person("张三", 25);Person p2 = new Person("张三", 25);System.out.println(p1.equals(p2)); // ✅ trueSet<Person> set = new HashSet<>();set.add(p1);System.out.println(set.contains(p2)); // ❌ false!}
}
❓ 为什么
contains(p2)
返回false
?
因为p1
和p2
虽然equals()
相等,但hashCode()
不同(继承自Object
),导致HashSet
认为它们是不同的“桶”,根本不会去比较。
✅ 正确重写 hashCode()
@Override
public int hashCode() {return Objects.hash(name, age);// 或手动计算// int result = name != null ? name.hashCode() : 0;// result = 31 * result + age;// return result;
}
✅
Objects.hash()
是最简单安全的方式。
📊 HashSet
工作原理图解
hashCode() → 决定“桶”(Bucket)
equals() → 在桶内比较“是不是同一个”Person p1: hashCode=123 → 桶A
Person p2: hashCode=456 → 桶B (即使 equals 相等,也不在同一个桶)重写后:
Person p1: hashCode=789 → 桶C
Person p2: hashCode=789 → 桶C
然后用 equals() 比较 → 相等 → contains 返回 true
✅ 记住:
equals()
相等 ⇒hashCode()
必须相等。
反过来不成立(哈希冲突)。
7. 面试常见陷阱大揭秘
❓ 面试题1:==
和 equals()
的区别?
✅ 答:
==
:基本类型比数值,引用类型比内存地址。equals()
:默认行为同==
,但通常被重写为比较逻辑内容。
❓ 面试题2:为什么重写 equals()
必须重写 hashCode()
?
✅ 答:为了满足
HashSet
/HashMap
的契约。
如果equals()
相等但hashCode()
不同,对象在哈希集合中无法被正确找到。
❓ 面试题3:String
的 ==
什么时候为 true
?
✅ 答:当两个
String
变量指向同一个对象时,包括:
- 字符串常量池中的同一个字符串。
- 同一个
new String()
对象的引用。intern()
返回的字符串。
❓ 面试题4:如何正确重写 equals()
?
✅ 答:遵循以下步骤:
- 用
==
检查自反性。- 检查
null
。- 用
getClass()
或instanceof
检查类型。- 转换类型。
- 逐个字段比较(注意
null
、浮点数、数组等)。- 同时重写
hashCode()
。
❓ 面试题5:Integer
的 ==
比较有什么陷阱?
✅ 答:
-128
到127
之间的Integer
有缓存,==
可能为true
;超出范围则为false
。
所以比较值应该用equals()
。
Integer a = 127, b = 127; System.out.println(a == b); // true
Integer c = 128, d = 128; System.out.println(c == d); // false
8. 最佳实践与使用场景总结
场景 | 推荐做法 | 原因 |
---|---|---|
比较基本类型 | 使用 == | 直接比较数值 |
比较引用类型是否为同一对象 | 使用 == | 检查内存地址 |
比较对象内容是否相等 | 使用 equals() | 逻辑相等性 |
自定义类 | 正确重写 equals() 和 hashCode() | 确保在集合中正常工作 |
比较字符串内容 | 使用 equals() | 安全、可靠 |
字符串常量比较 | 可用 == (但不推荐) | 因为常量池,但行为不稳定 |
性能敏感代码 | 避免不必要的 equals() 调用 | 方法调用有开销 |
✅ 黄金法则:
==
看“身份证”,equals()
看“长相”。- 重写
equals()
,必重写hashCode()
。- 比较内容用
equals()
,别用==
。- 善用
Objects.equals()
和Objects.hash()
。
📈 附录:==
vs equals()
速查表
比较类型 | == | equals() |
---|---|---|
基本类型 | 比较数值 | ❌ 不适用 |
引用类型 | 比较内存地址 | 比较逻辑内容(需重写) |
String 字面量相同 | ✅ true (常量池) | ✅ true |
String new 出来相同内容 | ❌ false | ✅ true |
Integer 127 | ✅ true (缓存) | ✅ true |
Integer 128 | ❌ false | ✅ true |
null 比较 | null == null → true | null.equals() → NPE |
💡 记住:
equals()
是方法,可以被重写;==
是运算符,行为固定。
理解它们,你就能在Java世界里“认人”不迷路!
🎯 总结一下:
本文深入探讨了《== 和 equals() 方法的区别与实现原理》,从原理到实践,解析了面试中常见的考察点和易错陷阱。掌握这些内容,不仅能应对面试官的连环追问,更能提升你在实际开发中的技术判断力。
🔗 下期预告:我们将继续深入Java面试核心,带你解锁《** hashCode() 和 equals() 为什么必须成对重写?**》 的关键知识点,记得关注不迷路!
💬 互动时间:你在面试中遇到过类似问题吗?或者对本文内容有疑问?欢迎在评论区留言交流,我会一一回复!
如果你觉得这篇文章对你有帮助,别忘了 点赞 + 收藏 + 转发,让更多小伙伴一起进步!我们下一篇见 👋