首页 / Java 入门教程 / Object 类:所有 Java 对象的老祖宗

Java 入门教程

Object 类:所有 Java 对象的老祖宗

本教程共 100 篇 · 第 54 篇 · 更新于 2026-08-05 · 约 7 分钟阅读

JavaJava 入门教程Object类equalshashCodetoString

54. Object 类

本节目标:搞懂 Java 里所有对象共同的”老祖宗” Object,以及它身上 equals、hashCode、toString、clone 这几个最常用的方法。

你写的每一个类,背后都悄悄”认”了一个祖宗。哪怕你什么都没写,只要 class Person {},Java 也默认让它继承自 Object。所以,Object 是所有类的根,class A extends Object 这句话你不写也成立。

在 Java 里,一个类只能有一个直接父类,但所有类的终点都指向 Object。所以 Object 上定义的方法,每个对象都天生带着:比较、取哈希、转字符串、克隆,全从这儿来。

为什么要有这样一个根类

试想如果没有公共父类,每个类都各写一套”比较是否相等”的逻辑,集合框架就没法统一处理。正因为大家都从 Object 长出来,ListSet、哈希表这些容器才能用同一套规则对待所有对象。

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,这是硬性规则。原因是:相等的对象,哈希值必须相等。

哈希值可以理解成对象在哈希表里的”门牌号”。HashMapHashSet 先按门牌号找桶,再用 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,它是不可变数据的快捷写法。编译器会自动生成 equalshashCodetoString 以及构造器和字段访问器,省得你手敲。讲到记录类时你会专门学,这里先记住:当你只是想用一个”装数据的盒子”时,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`。搞定这几个,后面的集合和字符串章节就顺了。