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

Java 入门教程

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

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

JavaJava 入门教程Object类equalshashCodetoString

本节目标:搞懂 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,内容相同

Object 自带的 equals 其实就长这样:

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);
    }
}

Objects.equals(name, p.name) 的好处是:两边都为 null 也不会抛空指针。注意 Java 16 之后还能用 record 让编译器自动生成合格的 equalshashCode,以后学到记录类会更省事。

equals 被集合偷偷用到

你以为 equals 只是自己调着玩的?其实集合框架到处依赖它。ListcontainsindexOf,以及 Set 的判重,背后都靠元素的 equals

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);
}

Objects.hash 内部会把各字段的哈希组合起来,你不用自己写那套位移异或的算式,既不易错又可读。

Warning

只重写 equals 不重写 hashCode,是新手最隐蔽的 bug。用 HashSet 存对象时,它可能”装进去却查不出来”。

toString:对象的”自我介绍”

直接 System.out.println(对象),打印出来的那串奇怪文字,就来自 toStringObject 默认实现长这样:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

也就是”类名@十六进制哈希”,人看不懂。调试时强烈建议重写,把关键字段打印出来:

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

现代 Java 也推荐用文本块或 String.format 拼,后面章节会讲。IDE 一般能一键生成 toString,但读懂它、自己会写更踏实。

getClass 与 wait/notify

Object 还有几个方法值得知道。getClass() 返回对象运行时的类信息,反射和热判断类型时常用。notifynotifyAllwait 是一组用于线程间协作的方法——它们必须在 synchronized 块里调用,配合锁来让线程等待或唤醒。

这些方法不属于入门日常,先有个印象即可:它们证明了 Object 之所以是”万物之根”,连线程通信的基底都放在了它身上。

clone:复制一个”双胞胎”

clone() 用来拷贝对象,但它有点特殊:它是 protected,且要求类实现 Cloneable 接口,否则会抛 CloneNotSupportedException

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 也跟着变了

b 看着是个新对象,可它的 membersa 指向同一个 ArrayList。改 b 就是改 a。要断开这层牵连,引用字段得自己再复制一份:

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)

Cloneable 是个没有任何方法的「标记接口」,它唯一的用处就是让 Object.clone() 别抛这个异常。

hashCode() 必须保证每个对象都不一样吗?

不必。契约只要求「相等的对象哈希值必须相等」,反过来不成立——两个不相等的对象撞上同一个哈希值是允许的,哈希表会用 equals 在桶里再确认一次。极端点写 return 1; 也不算违约,只是 HashMap 会退化成链表,查找变慢。

equals 里用 getClass() 还是 instanceof

差别在子类身上。getClass() != o.getClass() 要求类型严格一致,父类对象和子类对象永不相等,好处是对称性一定成立;instanceof 允许子类实例参与比较,写不好容易破坏对称性。类会被继承就用 getClass(),类是 final 或直接用 record 就随意。

一个常见误区

有人以为”重写了 equals 就不用管 hashCode”,这是错的。只要这个对象要进 HashMap/HashSet,两者就必须配套。还有人把 == 当成”值相等”去比对象,结果在包装类、字符串上屡屡翻车。记住:对象比内容永远用 equals,基本类型才用 ==

小结

Object 是一切类的根,equalshashCodetoStringclone 人人自带。记住三条铁律:比内容用 equals,重写 equals 必须重写 hashCode,调试记得重写 toString。搞定这几个,后面的集合和字符串章节就顺了。