本篇是 2017 年 Java 学习笔记系列的第十一天。前半段聊聊 Eclipse 这碗”老酒”怎么喝才高效,并附上它与当今主流 IntelliJ IDEA 的快捷键对照;后半段讲透 Java 万类之祖 Object——equals、hashCode、toString 这三个几乎天天见面的方法,默认实现有什么问题、为什么要重写、重写时必须遵守什么契约。

本篇目标

  • 了解 Eclipse 的核心高效操作,并能在 IntelliJ IDEA 中找到对应功能
  • 理解 Object 作为类层次结构根类的意义
  • 掌握 toString、equals、hashCode 的默认行为与重写动机
  • 记住 equals 与 hashCode 之间必须共同遵守的契约
  • 能手写一个”标准的”学生类

写在前面:这是一篇 Eclipse 时代的笔记

本文写于 2017 年,当时 Eclipse 是国内 Java 课堂的标配 IDE;如今业界主流早已换成 IntelliJ IDEA。工具会变,但”用熟 IDE”的思路不变:内容辅助、快速修复、代码重构、断点调试,这四板斧在哪个 IDE 里都是吃饭的家伙。Eclipse 里有几个概念放到今天依然值得记住:

  • 工作空间(Workspace):源代码按”工作空间 → 项目 → 包 → 类”的层级组织,IDEA 里对应 Project 与 Module。
  • 红色与黄色波浪线:红线是编译错误,必须处理,否则跑不起来;黄线是警告(如未使用的变量),可以先放一放,但好习惯是尽早清掉。
  • 断点调试:在想观察的行号旁打断点,以 Debug 方式启动,程序会停在断点处,之后单步执行(Eclipse 按 F6,IDEA 按 F8)并观察变量窗口的变化。这是排查逻辑错误的基本功,比在代码里到处打印变量高效得多——打印只能看到结果,断点能看到过程。

常用快捷键对照(Windows/Linux 键位,macOS 下 Ctrl 多对应 Cmd):

功能 Eclipse IntelliJ IDEA
内容辅助(代码提示) Alt + / Ctrl + Space
快速修复建议 Ctrl + 1 Alt + Enter
格式化代码 Ctrl + Shift + F Ctrl + Alt + L
优化导包 Ctrl + Shift + O 自动导入,或 Alt + Enter
删除当前行 Ctrl + D Ctrl + Y
上下移动代码行 Alt + ↑ / ↓ Alt + Shift + ↑ / ↓
全局查找类 Ctrl + Shift + T Ctrl + N
跳转查看源码 F3 或 Ctrl + 点击 Ctrl + B 或 Ctrl + 点击
重命名重构 Alt + Shift + R Shift + F6
抽取方法 Alt + Shift + M Ctrl + Alt + M
生成构造/getter/setter Alt + Shift + S Alt + Insert
单行注释 Ctrl + / Ctrl + /

不必一次全背下来,先把”提示、修复、格式化、调试”四个用熟,剩下的在用中记。

Object:所有类的共同祖先

Java 中每一个类都直接或间接继承自 Object——即使你没写 extends,编译器也会悄悄补上。这样设计的好处是:任何对象都拥有统一的基本行为(转成字符串、比较相等、计算哈希),任何引用都能用 Object 类型的变量接收,这是多态和整个集合框架得以成立的根基。之前讲构造方法时说”子类构造默认调用父类无参构造”,这条调用链向上追,终点就是 Object 的 public Object()。可以把它理解成 Java 世界里的”出生证明”:只要是个对象,就天然带着这套基础能力。

toString:对象的”名片”

默认实现等价于 getClass().getName() + "@" + Integer.toHexString(hashCode()),打印出来是 com.demo.Student@1b6d3586 这样的东西——类名加一串十六进制,对人类几乎没有信息量。为什么建议重写?因为 System.out.println(student) 实际调用的就是 student.toString(),重写之后,日志和调试输出才能直接看到对象的内容:

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}

equals 与 ==:到底什么叫”相等”

== 是运算符:比较基本类型时比值,比较引用类型时比的是地址。Object 默认的 equals 内部就是 ==,所以两个分别 new 出来的 Student,哪怕姓名、年龄完全相同,== 和默认 equals 都是 false——它们在堆中是两个对象。但业务上我们通常认为”字段都一样就是同一个人”,所以要重写 equals,改为比较内容。

重写 equals 必须满足五个契约(面试高频):

  1. 自反性:x.equals(x) 必须为 true;
  2. 对称性:x.equals(y) 与 y.equals(x) 结果一致;
  3. 传递性:x 等于 y、y 等于 z,则 x 等于 z;
  4. 一致性:对象没改动时,多次调用结果一致;
  5. 对 null:x.equals(null) 必须返回 false。

常见错误:把参数写成具体类型 public boolean equals(Student s)。这不是重写而是重载——HashSet 等集合调用的仍是 Object 版本的 equals,你的方法根本不会被执行。务必加上 @Override,让编译器替你检查。

hashCode:与 equals 的生死契约

hashCode 的默认实现按对象地址计算,不同对象一般不同。哈希容器(HashMap、HashSet)的工作方式是先用 hashCode 定位桶,再用 equals 在桶内逐个确认。它也有三条契约:

  1. 对象不改动时,多次调用 hashCode 结果必须一致;
  2. equals 为 true 的两个对象,hashCode 必须相等;
  3. equals 为 false 时 hashCode 不强制不同,但尽量不同能减少哈希冲突。

由此得出黄金法则:重写 equals 就必须同时重写 hashCode,且两个方法要用同一组字段计算。只重写 equals 的后果很隐蔽:两个”相等”的 Student 能同时放进 HashSet;作为 HashMap 的 key 存进去之后再 get,会因为定位到错误的桶而查不到——对象等于”丢”了。反过来只重写 hashCode 不重写 equals 也一样出问题:桶定位对了,桶内确认环节却仍按地址比较,照样找不到。这也是为什么 IDE 生成这两个方法时总是成对出现。

至于 getClass(),它返回对象的运行时类,是反射的入口,这里先知道名字,反射章节再展开。

经典练习:一个标准的学生类

把上面三个方法落实到代码里:

import java.util.Objects;

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

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

public String getName() { return name; }
public int getAge() { return age; }

@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一引用直接相等
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age); // 与 equals 用同一批字段
}

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}

三个要点:@Override 不要省;equals 与 hashCode 必须使用同一组字段;Objects.equals 和 Objects.hash 帮你处理了 null,比手写判空稳妥。验证方式:new 两个姓名年龄都相同的 Student,equals 应返回 true;把它们都放进 HashSet,集合里应只剩一个。日常开发中这些方法通常由 IDE 自动生成(对照表里的”生成”快捷键),但要看得懂它生成的每一行在遵守哪条契约。

小结

  • IDE 是放大镜不是拐杖:内容辅助、快速修复、重构、调试四板斧,在 Eclipse 和 IDEA 里一通百通。
  • Object 是所有类的根,equals、hashCode、toString 是它发给每个对象的”基础行为”。
  • 默认 equals 比地址、toString 打印地址,业务开发中一般都要重写成”按内容”。
  • 契约红线:equals 相等 ⇒ hashCode 必须相等;两个方法永远一起重写、用同一组字段。