本节摘要:所有类都直接或间接继承 Object,它给全体住户立了通用契约——equals 判等内容、hashCode 出索引号、toString 出名片、getClass 报出身。契约的核心条款是:equals 相等的两个对象,hashCode 必须相同。本节先看默认行为的来历,再完整复现"重写 equals 不重写 hashCode 导致 HashSet 查无此人"的事故,这是第 5 章集合框架的地基。
刚学 Java 的人都干过这件事:System.out.println(对象),期待看到内容,结果得到类似 Resident@1b6d3586 的一串。这不是乱码,是 Object 的 toString 默认实现:类名加艾特符号加哈希码的十六进制。它的含义是"我是 Resident 家族、档案编号约是这一串"——对 JVM 有意义,对业务无意义。
Object 里的方法不多,但个个是全体类的默认配置。追根溯源:你写 class Resident 却从不写 extends,编译器自动补上 extends Object;而一个类什么都不继承时,它拿到的正是 Object 这份"出厂档案"。四个最常用的成员各司其职:
| 方法 | 默认行为 | 常见改写理由 |
|---|---|---|
| toString | 类名加哈希码十六进制 | 打印与日志里显示业务字段 |
| equals | 与双等号相同 比引用 | 按业务内容判等 |
| hashCode | 由内存地址换算的整数 | 配合 equals 重写 供哈希容器定位 |
| getClass | 返回运行时类对象 | 反射入口 第 8 章 8.3 节的主角 |
equals 的默认实现值得单独看一眼,它就是双等号的语义:
Object a = new Object(); Object b = new Object(); System.out.println(a.equals(b)); // 输出:false —— 默认 equals 比的是"是不是同一份档案" System.out.println(a.equals(a)); // 输出:true —— 同一份档案自然相等
业务判等几乎总要改 equals。两份"姓名加身份证号相同"的登记记录,业务上是同一个人,默认 equals 却说是两个——因为它比的是档案编号。重写 equals 的标准套路有五步:自反、对称、传递、一致、与 null 比为 false,工程上用下面这个模板逐条落:
class Person { final String idNo; // 身份证号:业务唯一标识 final String name; Person(String idNo, String name) { this.idNo = idNo; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; // 第一步 同一档案直接相等 if (o == null || getClass() != o.getClass()) // 第二步 空对象或不同家族 return false; Person p = (Person) o; // 第三步 向下转型(2.2 节) return idNo.equals(p.idNo); // 第四步 逐字段比对关键项 } @Override public int hashCode() { // 契约另一半:同步重写 return idNo.hashCode(); // 用关键字段的哈希 } @Override public String toString() { return name + "(" + idNo + ")"; } } public class EqDemo { public static void main(String[] args) { Person p1 = new Person("110101", "张三"); Person p2 = new Person("110101", "张三"); System.out.println(p1 == p2); // 输出:false 两份不同档案 System.out.println(p1.equals(p2)); // 输出:true 业务上是同一个人 System.out.println(p1); // 输出:张三(110101) } }
为什么 hashCode 必须跟着改?这就是 Object 契约的核心条款:equals 相等的两个对象,hashCode 必须相等;反过来不要求。原因在哈希容器的查找方式:HashSet、HashMap 先拿 hashCode 定位到某个桶,再在桶里用 equals 逐个核对。只改 equals 不改 hashCode,两个"业务相等"的对象会拿到两个不同的默认哈希值,被分进不同的桶——容器在 A 桶里找,对象却在 B 桶里躺着,查询扑空。

背景:参会系统用 HashSet 存报名者做查重,Person 类重写了 equals(按身份证号判等)但忘了重写 hashCode。操作:放入两条"业务上同一人"的记录,再查询其中一条:
import java.util.HashSet; import java.util.Set; public class LostDemo { public static void main(String[] args) { Set<Person> set = new HashSet<>(); Person first = new Person("110101", "张三"); set.add(first); set.add(new Person("110101", "张三")); // 以为会被判重复拦下 System.out.println("集合大小:" + set.size()); // 实测输出:集合大小:2 —— 重复登记了! System.out.println("查询结果:" + set.contains(first)); // 输出:true Person probe = new Person("110101", "张三"); // 换一份内容相同的档案去查 System.out.println("换档案查询:" + set.contains(probe)); // 输出:false —— 查无此人 System.out.println("删除结果:" + set.remove(probe)); // 输出:false —— 也删不掉 } }
结果:集合里躺着两条"同一个人",且用内容相同的新档案查询、删除都失败。解读:contains 与 remove 的第一步都是算 probe 的 hashCode。probe 与 first 的 equals 为 true,但默认 hashCode 各不相同(不同档案各自换算),probe 被指到另一个桶,那个桶里没有目标,直接返回 false——equals 根本没机会执行。补上本节前面 hashCode 的重写后,三行输出变成 1、true、true。变式:把判定字段从身份证号换成可变的 name,对象入集合后再改名,同样会出现定位漂移——这引出"不要用可变对象做哈希键"的铁律,第 5 章 5.3 节会正面展开。
族谱科到此办结:继承给骨架、多态给活力、Object 给契约。但族谱解决不了"横切能力"——鸟、飞机、超人都会飞,却共不着任何祖先。下一章进发证科:抽象类当半成品模板,接口发执业资格证,一个系统怎么搭配这两件东西,是面向对象设计的分水岭。