Object 类:所有 Java 对象的老祖宗
本教程共 100 篇 · 第 54 篇 · 更新于 2026-08-05 · 约 7 分钟阅读
54. Object 类
本节目标:搞懂 Java 里所有对象共同的”老祖宗” Object,以及它身上 equals、hashCode、toString、clone 这几个最常用的方法。
你写的每一个类,背后都悄悄”认”了一个祖宗。哪怕你什么都没写,只要 class Person {},Java 也默认让它继承自 Object。所以,Object 是所有类的根,class A extends Object 这句话你不写也成立。
在 Java 里,一个类只能有一个直接父类,但所有类的终点都指向 Object。所以 Object 上定义的方法,每个对象都天生带着:比较、取哈希、转字符串、克隆,全从这儿来。
为什么要有这样一个根类
试想如果没有公共父类,每个类都各写一套”比较是否相等”的逻辑,集合框架就没法统一处理。正因为大家都从 Object 长出来,List、Set、哈希表这些容器才能用同一套规则对待所有对象。
Object 位于 java.lang 包,这个包的内容编译器会自动帮你导入,所以你从来不需要自己写 import java.lang.Object;。
它身上最常用的方法是这几个:equals(Object)、hashCode()、toString()、clone()。下面一个一个说。
Object 方法速查
先把全家福摆出来,心里有个地图:
| 方法 | 作用 | 要不要重写 |
|---|---|---|
equals(Object) | 判断内容是否相等 | 有值语义就要重写 |
hashCode() | 返回哈希值,供哈希容器分桶 | 重写 equals 就必须一起重写 |
toString() | 对象的字符串描述 | 强烈建议重写,调试全靠它 |
getClass() | 拿运行时类型 | 不能重写,它是 final |
clone() | 浅拷贝一份对象 | 尽量别用,改用复制构造器 |
wait / notify / notifyAll | 线程间等待与唤醒 | 不能重写,都是 final |
Object 还有个 finalize(),它自 Java 9 起已废弃、Java 18 起默认禁用,不要用、也不必了解它的写法。资源清理请用 try-with-resources。
equals:判断”长得像不像”
很多新手分不清 == 和 equals。记住一句话:== 比的是”是不是同一个东西”,equals 比的是”内容像不像”。
String a = new String("hi");
String b = new String("hi");
System.out.println(a == b); // false,两个不同对象
System.out.println(a.equals(b)); // true,内容相同
```java
`Object` 自带的 `equals` 其实就长这样:
```java
public boolean equals(Object obj) {
return (this == obj);
}
也就是说,默认情况下它和 == 没区别,只判断是不是同一个对象。要让两个”内容相同但不同对象”被判为相等,你得自己重写它。
重写 equals 要守的规矩
写一个靠谱的 equals 不容易。Java 规定了几条契约,违反会让集合出错:
- 自反:自己等于自己,
x.equals(x)必须 true。 - 对称:你等于我,我也要等于你。
- 传递:甲等于乙、乙等于丙,则甲等于丙。
- 一致:对象没改,反复比结果要一样。
- 对 null 永远返回 false。
推荐用 instanceof 配合 Objects.equals 来写,既安全又省心:
import java.util.Objects;
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person p)) return false;
return age == p.age && Objects.equals(name, p.name);
}
}
```bash
`Objects.equals(name, p.name)` 的好处是:两边都为 null 也不会抛空指针。注意 Java 16 之后还能用 `record` 让编译器自动生成合格的 `equals` 和 `hashCode`,以后学到记录类会更省事。
## equals 被集合偷偷用到
你以为 `equals` 只是自己调着玩的?其实集合框架到处依赖它。`List` 的 `contains`、`indexOf`,以及 `Set` 的判重,背后都靠元素的 `equals`。
```java
List<Person> list = new ArrayList<>();
list.add(new Person("小明", 18));
System.out.println(list.contains(new Person("小明", 18))); // 取决于 equals
如果 Person 没重写 equals,上面就会返回 false——因为两个新对象地址不同。这正是”为什么一定要重写 equals”最实在的理由。
hashCode:对象的”门牌号”
如果你重写了 equals,就必须重写 hashCode,这是硬性规则。原因是:相等的对象,哈希值必须相等。
哈希值可以理解成对象在哈希表里的”门牌号”。HashMap、HashSet 先按门牌号找桶,再用 equals 在桶里确认。两个内容相等的对象若门牌号不同,哈希表就会把它们当成两家人,去重、查找全乱套。
最简单的写法是用 Objects.hash() 把关键字段打包:
@Override
public int hashCode() {
return Objects.hash(name, age);
}
```bash
`Objects.hash` 内部会把各字段的哈希组合起来,你不用自己写那套位移异或的算式,既不易错又可读。
> [!WARNING]
> 只重写 `equals` 不重写 `hashCode`,是新手最隐蔽的 bug。用 `HashSet` 存对象时,它可能"装进去却查不出来"。
## toString:对象的"自我介绍"
直接 `System.out.println(对象)`,打印出来的那串奇怪文字,就来自 `toString`。`Object` 默认实现长这样:
```java
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
也就是”类名@十六进制哈希”,人看不懂。调试时强烈建议重写,把关键字段打印出来:
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
```bash
现代 Java 也推荐用文本块或 `String.format` 拼,后面章节会讲。IDE 一般能一键生成 `toString`,但读懂它、自己会写更踏实。
## getClass 与 wait/notify
`Object` 还有几个方法值得知道。`getClass()` 返回对象运行时的类信息,反射和热判断类型时常用。`notify`、`notifyAll`、`wait` 是一组用于线程间协作的方法——它们必须在 `synchronized` 块里调用,配合锁来让线程等待或唤醒。
这些方法不属于入门日常,先有个印象即可:它们证明了 `Object` 之所以是"万物之根",连线程通信的基底都放在了它身上。
## clone:复制一个"双胞胎"
`clone()` 用来拷贝对象,但它有点特殊:它是 `protected`,且要求类实现 `Cloneable` 接口,否则会抛 `CloneNotSupportedException`。
```java
class Point implements Cloneable {
int x, y;
@Override
protected Point clone() throws CloneNotSupportedException {
return (Point) super.clone();
}
}
Note
clone()是”浅拷贝”:只复制对象本身,字段里的引用还是指向原来的对象。要深拷贝得自己手动处理。正因为它用起来坑多,实际项目里更多人选择用”复制构造器”或改造函数,而不是clone()。
record:编译器替你写好这些
Java 16 起有了 record,它是不可变数据的快捷写法。编译器会自动生成 equals、hashCode、toString 以及构造器和字段访问器,省得你手敲。讲到记录类时你会专门学,这里先记住:当你只是想用一个”装数据的盒子”时,record 能把本章讲的几个方法一次性包办,而且生成的质量比手写更稳。
浅拷贝到底浅在哪
clone() 的坑不是抽象的,跑一遍就明白:
class Team implements Cloneable {
List<String> members = new ArrayList<>();
@Override
protected Team clone() throws CloneNotSupportedException {
return (Team) super.clone();
}
}
Team a = new Team();
a.members.add("小明");
Team b = a.clone();
b.members.add("小红");
System.out.println(a.members); // [小明, 小红] —— a 也跟着变了
```java
`b` 看着是个新对象,可它的 `members` 和 `a` 指向同一个 `ArrayList`。改 `b` 就是改 `a`。要断开这层牵连,引用字段得自己再复制一份:
```java
Team copy = (Team) super.clone();
copy.members = new ArrayList<>(this.members); // 手动深拷贝
return copy;
更省心的路子是干脆不碰 Cloneable,写个复制构造器:Team(Team other) { this.members = new ArrayList<>(other.members); }。意图清楚、没有受检异常、也不用强制转型。
常见疑问
忘了实现 Cloneable 会怎样?
编译能过,一跑就炸:
Exception in thread "main" java.lang.CloneNotSupportedException: Team
at java.base/java.lang.Object.clone(Native Method)
```bash
`Cloneable` 是个没有任何方法的「标记接口」,它唯一的用处就是让 `Object.clone()` 别抛这个异常。
**`hashCode()` 必须保证每个对象都不一样吗?**
不必。契约只要求「相等的对象哈希值必须相等」,反过来不成立——两个不相等的对象撞上同一个哈希值是允许的,哈希表会用 `equals` 在桶里再确认一次。极端点写 `return 1;` 也不算违约,只是 `HashMap` 会退化成链表,查找变慢。
**`equals` 里用 `getClass()` 还是 `instanceof`?**
差别在子类身上。`getClass() != o.getClass()` 要求类型严格一致,父类对象和子类对象永不相等,好处是对称性一定成立;`instanceof` 允许子类实例参与比较,写不好容易破坏对称性。类会被继承就用 `getClass()`,类是 `final` 或直接用 record 就随意。
## 一个常见误区
有人以为"重写了 `equals` 就不用管 `hashCode`",这是错的。只要这个对象要进 `HashMap`/`HashSet`,两者就必须配套。还有人把 `==` 当成"值相等"去比对象,结果在包装类、字符串上屡屡翻车。记住:对象比内容永远用 `equals`,基本类型才用 `==`。
## 小结
`Object` 是一切类的根,`equals`、`hashCode`、`toString`、`clone` 人人自带。记住三条铁律:比内容用 `equals`,重写 `equals` 必须重写 `hashCode`,调试记得重写 `toString`。搞定这几个,后面的集合和字符串章节就顺了。