== 与 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 == s2true

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 在缓存范围内,ab 指向同一个缓存对象,a == btrue
  • 128 超出缓存范围,cd 是两个不同的新对象,c == dfalse
⚠️

绝不用 == 比较包装类对象,始终用 equals()。这是生产中真实发生过的 bug:在数值 <= 127 时测试通过,上生产后数值超过 127 导致比较错误。

二、equals 的本质 🔴

2.1 Object.equals 默认实现

// Object 类的 equals 默认实现
public boolean equals(Object obj) {
    return (this == obj); // 默认比较引用!
}

Object 的 equals 默认也是比较引用!

只有子类重写equals,才能比较"值"。StringIntegerArrayList 等都重写了 equals

2.2 equals 五大约定(必考)

equals 必须满足:

约定含义
自反性x.equals(x) 必须为 true
对称性x.equals(y)true,则 y.equals(x) 也为 true
传递性x.equals(y)y.equals(z),则 x.equals(z)
一致性多次调用结果必须一致(前提是对象未变化)
非空性x.equals(null) 必须为 false

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)。

【面试官心理】 第四层追问是关键。equalshashCode 的联动关系是 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,没有去重!

原因:HashSethashCode 找桶,再用 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"

同样的原因:u1u2 没有重写 hashCode,hash 值不同,get 找不到正确的桶。

⚠️

重写 equals 必须同时重写 hashCode,这是 Java 规范的强制约定。所有 IDE 的"Generate equals and hashCode"功能都会同时生成两者。