== 与 equals 有什么区别
90% 的候选人以为这道题会送分,但面试官真正要考的不是概念,是细节。
面试官问:"== 和 equals 有什么区别?"
小张脱口而出:"== 比较引用地址,equals 比较值。"
面试官点点头:"那这两行代码,哪个输出 true?"
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // ?
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // ?
小张愣了两秒:"都是 false?"
面试官:"不对。"
【面试官心理】
我问这道题,是在考候选人对 Java 自动装箱机制和整数缓存池的理解。能背出概念的人占 90%,能答对整数缓存池的只有 30%。这两道题直接拉开 P5 和 P6 的差距。
一、== 的本质 🔴
1.1 两种含义
== 对基本类型比较的是值,对引用类型比较的是堆内存地址(引用)。
// 基本类型:比较值
int a = 10;
int b = 10;
System.out.println(a == b); // true(值相等)
// 引用类型:比较地址
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(不同对象,不同地址)
1.2 字符串常量池陷阱
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(值相同)
原因:Java 字符串字面量会在编译期被放入字符串常量池,相同字面量指向同一个池中对象,所以 s1 == s2 为 true。
1.3 整数缓存池(必考陷阱)
回到开头的问题:
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 a = 127 等价于 Integer a = Integer.valueOf(127)。
Integer.valueOf() 对 -128 ~ 127 范围内的整数做了缓存:
// Integer.valueOf() 源码(简化)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回缓存对象
return new Integer(i); // 超出范围,新建对象
}
127 在缓存范围内,a 和 b 指向同一个缓存对象,a == b 为 true
128 超出缓存范围,c 和 d 是两个不同的新对象,c == d 为 false
⚠️
绝不用 == 比较包装类对象,始终用 equals()。这是生产中真实发生过的 bug:在数值 <= 127 时测试通过,上生产后数值超过 127 导致比较错误。
二、equals 的本质 🔴
2.1 Object.equals 默认实现
// Object 类的 equals 默认实现
public boolean equals(Object obj) {
return (this == obj); // 默认比较引用!
}
Object 的 equals 默认也是比较引用!
只有子类重写了 equals,才能比较"值"。String、Integer、ArrayList 等都重写了 equals。
2.2 equals 五大约定(必考)
equals 必须满足:
2.3 错误的 equals 实现
// ❌ 违反对称性的 equals
class SmartDate {
private int year, month, day;
@Override
public boolean equals(Object obj) {
if (obj instanceof SmartDate) {
SmartDate d = (SmartDate) obj;
return year == d.year && month == d.month && day == d.day;
}
if (obj instanceof java.util.Date) {
// 尝试和 java.util.Date 比较
// ...
}
return false;
}
}
// 问题:SmartDate.equals(Date) 可能为 true
// 但 Date.equals(SmartDate) 一定为 false(Date 不认识 SmartDate)
// 违反了对称性!
2.4 标准的 equals 实现模板
@Override
public boolean equals(Object obj) {
// 1. 自反性检查(性能优化)
if (this == obj) return true;
// 2. null 检查和类型检查
if (obj == null || getClass() != obj.getClass()) return false;
// 3. 字段比较
User other = (User) obj;
return id == other.id
&& Objects.equals(name, other.name);
}
💡
使用 getClass() != obj.getClass() 而非 instanceof,是为了在继承体系中保证对称性。IDEA 自动生成的 equals 就是这个模式。
三、String 的特殊情况 🔴
// 字符串比较的正确姿势
String a = "hello";
String b = new String("hello");
String c = b.intern(); // 从常量池获取
System.out.println(a == b); // false(a 在池中,b 在堆)
System.out.println(a == c); // true(c 从池中获取,和 a 同一对象)
System.out.println(a.equals(b)); // true(值相同)
intern() 方法:如果字符串常量池中已存在该值,返回池中引用;否则将其放入池中再返回。
⚠️
永远用 equals 比较字符串,不要用 ==。如果确实需要用 == 比较(如性能优化),必须确保两个字符串都经过了 intern() 处理。
四、面试追问链
第一层:"== 和 equals 的区别?"
→ == 对基本类型比值,对引用类型比地址;equals 默认比地址,子类重写后比值。
第二层:"没有重写 equals 的自定义类,两个内容相同的对象 equals 返回什么?"
→ false。因为没有重写,用的是 Object.equals,还是比地址。
第三层:"Integer 的 == 比较结果和数值范围有什么关系?"
→ -128 ~ 127 范围内缓存,== 为 true;超出范围 == 为 false。
第四层:"如果要正确重写 equals,需要同时重写什么?"
→ 必须同时重写 hashCode。否则在 HashMap、HashSet 等集合中会出现逻辑错误(equal 的对象必须有相同的 hashCode)。
【面试官心理】
第四层追问是关键。equals 和 hashCode 的联动关系是 Java 集合框架的核心契约,没有理解这个的候选人,在集合框架题上也会翻车。能主动说出"必须同时重写 hashCode"的候选人,直接拉开档次。
五、生产避坑
5.1 集合去重失效
// 没有重写 equals/hashCode 的 User 类
class User {
int id;
String name;
User(int id, String name) { this.id = id; this.name = name; }
}
Set<User> users = new HashSet<>();
users.add(new User(1, "Alice"));
users.add(new User(1, "Alice")); // 期望去重
System.out.println(users.size()); // 输出 2,没有去重!
原因:HashSet 用 hashCode 找桶,再用 equals 判断是否重复。没有重写这两个方法,两个 User 对象 hash 不同,直接放入不同桶,不会触发 equals 比较。
5.2 Map 查找失效
Map<User, String> map = new HashMap<>();
User u1 = new User(1, "Alice");
map.put(u1, "admin");
User u2 = new User(1, "Alice");
System.out.println(map.get(u2)); // null!而非 "admin"
同样的原因:u1 和 u2 没有重写 hashCode,hash 值不同,get 找不到正确的桶。
⚠️
重写 equals 必须同时重写 hashCode,这是 Java 规范的强制约定。所有 IDE 的"Generate equals and hashCode"功能都会同时生成两者。