×

Java 里有一个类,是所有类的最终祖先——上到 String、下到你自定义的 Dog 类,都是它的子孙。它就是 java.lang.Object

Object 在你刚学 Java 的时候可能没怎么在意它——因为你定义类从来不写 extends Object,但编译器默默地帮你补上了。Java 里写 class A {},编译后等价于 class A extends Object {}

Object 类里面一共就 11 个方法(除了过时的 finalize() 也就 10 个)。这篇我们先聊两个面试问得最多的:equals()hashCode()

一、equals() VS ==

这问题但凡面过 Java 的都避不开:==equals() 有什么区别?

1.1 原理

  • ==:比较基本类型的值,或者引用类型的内存地址(即是不是同一个对象)
  • equals():默认也是比较地址(Object 的实现就是 ==),但很多类重写过它,改为比较内容
String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);          // false —— 两个不同的对象
System.out.println(s1.equals(s2));     // true  —— String 重写了 equals,比较内容

这个坑我有一次在写登录校验的时候踩过:从数据库查出来的密码和用户输入的密码用 == 比较,结果怎么也登不上去……排查了半个小时才发现是 == 的问题。String 是引用类型,不能用 == 比较内容。

1.2 自己重写 equals()

默认的 equals() 只比较地址,所以自定义的类需要自己重写:

public class Student {
    private String id;     // 学号
    private String name;
    private int age;

    public Student(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        // 1. 比较地址:如果就是同一个对象,直接返回 true
        if (this == obj) return true;

        // 2. 判空和类型检查
        if (obj == null || getClass() != obj.getClass()) return false;

        // 3. 转型后按字段比较
        Student other = (Student) obj;
        return age == other.age
                && Objects.equals(id, other.id)
                && Objects.equals(name, other.name);
    }
}

重写 equals() 时要遵守几条约定(Java 语言规范写的):

  • 自反性x.equals(x) 必须返回 true
  • 对称性x.equals(y) 等于 y.equals(x)
  • 传递性:如果 x.equals(y)y.equals(z),那 x.equals(z) 也必须 true
  • 一致性:只要对象没变,多次调用 equals() 结果应该一致
  • 非空性x.equals(null) 必须返回 false

其实真要手写的话,推荐用 Objects.equals() 工具方法,它能自动处理 null

// Objects.equals(a, b) 等价于 (a == b) || (a != null && a.equals(b))
// 不用手写空指针判断了

二、hashCode()——哈希表的基础

2.1 什么是哈希值?

hashCode() 返回一个 int 整数,可以理解为对象的一个数字摘要。默认实现(Object 类)是根据内存地址算出来的一个值。

但它真正的威力在哈希表里——HashMapHashSetHashTable 这些集合用 hashCode 来决定对象存到哪个桶(bucket)里。

2.2 equals 和 hashCode 的约定

这是 Java 里最最最重要的约定之一:

如果两个对象 equals() 返回 true,那么它们的 hashCode() 必须 相等。
如果两个对象 hashCode() 相等,equals() 不一定 true(哈希冲突)。

简单理解:hashCode 决定你住哪个小区,equals 决定你是不是同一个人。两个人住同一个小区不代表是同一个人,但同一个人一定住在同一个小区。

所以重写 equals() 的时候必须同时重写 hashCode()——否则你的对象放进 HashSetHashMap 会出大问题。

来看反面教材:

public class Student {
    String id;
    String name;

    // 只重写了 equals,没重写 hashCode ❌
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student s = (Student) o;
        return Objects.equals(id, s.id);
    }
}

// 使用 HashSet
Set<Student> set = new HashSet<>();
set.add(new Student("001", "张三"));
System.out.println(set.contains(new Student("001", "张三")));
// 输出:false ❌❌❌
// 明明两个 "001" 学号的学生 equals 相等,但因为 hashCode 不同,
// 存入和查找时去了不同的桶,HashSet 根本找不到那个对象!

这个 Bug 我在实习的时候亲自踩过——自定义对象放了 HashSet 里,怎么也查不到,翻了半天文档才意识到 hashCode 没重写。后来我给自己定了个铁律:记性再差,也要记住重写 equals 就必须重写 hashCode

2.3 正确重写 hashCode

标准写法(Java 7+ 推荐 Objects.hash()):

public class Student {
    String id;
    String name;
    int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student s = (Student) o;
        return age == s.age
                && Objects.equals(id, s.id)
                && Objects.equals(name, s.name);
    }

    @Override
    public int hashCode() {
        // 用 equals 用到的所有字段来计算 hashCode
        return Objects.hash(id, name, age);
    }
}

Objects.hash() 内部会把这几个字段组合成一个 hash 值。注意:hashCode 计算必须用 equals 中比较的那些字段,多一个少一个都不对。

三、IDE 一键生成:别手写

现在正经 Java 开发谁还手写 equals()hashCode()

IntelliJ IDEA / Eclipse / VS Code 都支持一键生成:右键 → Generate → equals() and hashCode() → 选好字段 → 搞定。

但理解原理很重要——你总得知道 IDE 给你生成了什么,以及为什么要这么写,不然面试一问就露馅。

四、小结

  • == 比较基本类型的值/引用类型的地址;equals() 默认比较地址,但可重写为比较内容
  • 重写 equals() 要满足自反性、对称性、传递性、一致性、非空性
  • 重写 equals() 必须重写 hashCode(),否则 HashMap/HashSet 行为异常
  • 优先用 Objects.equals()Objects.hash() 工具方法,避免自己造轮子
  • IDE 可以生成,但面试要手写——别光会用,原理得懂

下一篇我们把 Object 类剩下的方法讲完——toString()clone()getClass()finalize(),还有一个跟线程相关的 notify()/wait() 系列放多线程再说。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章