前面学完抽象类、接口、内部类之后,接下来非常值得继续掌握的一个知识点,就是 Object 类中的几个核心方法:

  • equals()
  • hashCode()
  • toString()

这三个方法在 Java 开发中出现频率非常高。

可以这么说:

  • 不理解 equals(),你就容易在“对象比较”上出错;
  • 不理解 hashCode(),你就很难真正弄明白 HashMapHashSet
  • 不理解 toString(),你打印对象时看到的内容就总是一串看不懂的地址值风格字符串。

所以这部分内容,不只是面试常考,在真实开发里也非常常用。


一、Object 类是什么?

Object 是 Java 中所有类的根类。

也就是说,任何一个类,如果没有明确继承其他类,那么默认都会继承 Object

class Student {
}

上面这个类,其实等价于:

class Student extends Object {
}

因此,任何 Java 对象,天然都可以使用 Object 类中的方法,比如:

  • equals()
  • hashCode()
  • toString()
  • getClass()
  • wait()
  • notify()

其中最常用、最基础的,就是今天要讲的这三个。


二、equals() 方法详解

1. equals() 是干什么的?

equals() 用来比较两个对象是否“相等”。

但是这里的“相等”,到底比较什么,要分情况看。

先看代码:

String s1 = new String("abc");
String s2 = new String("abc");

System.out.println(s1 == s2);      // false
System.out.println(s1.equals(s2)); // true

这里就说明:

  • == 比较的是两个变量保存的地址是否相同
  • equals() 比较的是两个对象的内容是否相同(前提是类重写了 equals 方法)

2. == 和 equals() 的区别

这是初学阶段最容易混淆的地方。

(1)对于基本数据类型

== 比较的是值。

int a = 10;
int b = 10;
System.out.println(a == b); // true
(2)对于引用数据类型

== 比较的是地址值。

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false

因为 s1s2 指向的是两个不同对象。

(3)equals() 默认也是比较地址

很多同学以为 equals() 天生就是比较内容,其实不是。

Object 类中默认的 equals() 实现,本质上还是比较地址。

你可以简单理解为:

public boolean equals(Object obj) {
    return (this == obj);
}

也就是说,如果一个类没有重写 equals(),那么它的效果和 == 差不多。


3. 为什么很多类的 equals() 比较内容?

因为像 StringInteger 这些类,已经重写了 equals() 方法。

例如 String 重写后,比较的是字符串内容。

String s1 = new String("java");
String s2 = new String("java");
System.out.println(s1.equals(s2)); // true

这就是因为 String 类把“两个字符串内容相同”定义为了相等。


4. 自定义类为什么要重写 equals()?

来看一个例子:

class Student {
    String name;
    int age;

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

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("张三", 18);

        System.out.println(s1.equals(s2));
    }
}

输出结果是:

false

为什么?

因为 Student 没有重写 equals(),所以调用的是 Object 的实现,本质比较的是地址。

但从业务角度看:

  • 姓名相同
  • 年龄相同

我们通常会认为这两个学生对象“内容相等”。

这时就应该重写 equals()


5. equals() 的重写示例

import java.util.Objects;

class Student {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Student student = (Student) obj;
        return age == student.age && Objects.equals(name, student.name);
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("张三", 18);

        System.out.println(s1.equals(s2)); // true
    }
}

6. equals() 重写时常见写法解释

this == obj

表示如果两个引用本来就指向同一个对象,那肯定相等,直接返回 true

obj == null

如果传进来的对象是 null,那一定不相等。

getClass() != obj.getClass()

表示只有类型完全相同,才继续比较。

强制类型转换
Student student = (Student) obj;

因为传入参数类型是 Object,需要转成当前类后才能访问属性。

比较成员变量
return age == student.age && Objects.equals(name, student.name);

这一步才是真正的“内容比较”。


7. equals() 使用时的注意事项

(1)重写 equals() 时,通常也要重写 hashCode()

这是一个非常重要的规则,后面会详细讲。

(2)尽量使用 Objects.equals() 比较引用类型

因为这样可以避免空指针异常。

Objects.equals(name, student.name)

比下面这种更安全:

name.equals(student.name)

因为如果 namenull,后者会报错。


三、hashCode() 方法详解

1. hashCode() 是什么?

hashCode() 方法返回的是对象的哈希值,也可以理解为一个整数“编号”。

这个值不是对象地址本身,但通常和对象的存储、查找效率有关。

Object obj = new Object();
System.out.println(obj.hashCode());

每个对象都可以调用这个方法。


2. hashCode() 有什么用?

它最主要的用途是:提高哈希结构中的查找效率。

比如:

  • HashMap
  • HashSet
  • Hashtable

这些集合在存储对象时,并不是一个一个慢慢比较,而是先根据 hashCode() 找位置,再进一步比较。

也就是说:

  • 先看哈希值
  • 再决定放到哪个位置
  • 如果哈希冲突,再调用 equals() 进一步比较

所以:

hashCode() 主要服务于哈希类集合。


3. equals() 和 hashCode() 的关系

这是最核心的内容之一。

Java 规范要求:

规则 1

如果两个对象通过 equals() 比较结果为 true,那么它们的 hashCode() 必须相同。

规则 2

如果两个对象的 hashCode() 相同,equals() 不一定为 true

也就是说:

  • 相等对象,哈希值必须相等
  • 哈希值相等,对象不一定相等

4. 为什么重写 equals() 后还要重写 hashCode()?

因为如果你只重写了 equals(),没有重写 hashCode(),那么在哈希集合中可能会出现逻辑错误。

看例子:

import java.util.HashSet;
import java.util.Objects;

class Student {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Student student = (Student) obj;
        return age == student.age && Objects.equals(name, student.name);
    }
}

public class Test {
    public static void main(String[] args) {
        HashSet<Student> set = new HashSet<>();
        set.add(new Student("张三", 18));
        set.add(new Student("张三", 18));

        System.out.println(set.size());
    }
}

很多人以为结果会是 1,但实际上可能是 2

原因就在于:

  • equals() 认为这两个对象相等
  • 但它们没有相同的哈希值规则
  • HashSet 可能先把它们分到不同位置
  • 于是去重失败

5. hashCode() 的正确重写方式

Java 中通常直接使用 Objects.hash(...)

import java.util.Objects;

class Student {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Student student = (Student) obj;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

这样写后:

  • 只要 nameage 相同
  • equals() 返回 true
  • hashCode() 也会一致

这就满足规范要求了。


6. 一句话理解 hashCode()

你可以把 hashCode() 理解成:

给对象生成一个“分类编号”,方便哈希集合快速定位。

equals() 则是:

当两个对象被分到同一区域后,再进一步精确比较它们是否真的相等。

所以两者经常配合使用。


四、toString() 方法详解

1. toString() 是干什么的?

toString() 方法用于返回对象的字符串表示形式。

例如:

Object obj = new Object();
System.out.println(obj.toString());
System.out.println(obj);

上面两句输出效果本质是一样的,因为直接打印对象时,Java 会自动调用对象的 toString() 方法。


2. Object 默认的 toString() 长什么样?

默认情况下,Object 类中的 toString() 返回结果格式大致如下:

类名@十六进制哈希值

比如:

Student@1b6d3586

这个结果对于程序运行没问题,但对人阅读不友好。

所以在开发中,我们通常会重写 toString(),让对象打印时直接显示关键属性。


3. 为什么要重写 toString()?

看下面例子:

class Student {
    String name;
    int age;

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

public class Test {
    public static void main(String[] args) {
        Student s = new Student("李四", 20);
        System.out.println(s);
    }
}

输出类似:

Student@6d06d69c

这对调试和日志输出帮助不大。

如果重写 toString()

class Student {
    String name;
    int age;

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

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

再打印:

Student{name='李四', age=20}

这样就直观多了。


4. toString() 的典型使用场景

(1)打印对象
System.out.println(student);
(2)调试程序

当你想快速看对象内部数据时,重写 toString() 非常方便。

(3)日志输出

在记录日志时,输出对象信息会更加清晰。


五、三个方法放在一起理解

我们用一个完整例子,把 equals()hashCode()toString() 串起来。

import java.util.Objects;

class Student {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Student student = (Student) obj;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

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

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("王五", 19);
        Student s2 = new Student("王五", 19);

        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println(s1);
    }
}

输出可能类似:

true
12345678
12345678
Student{name='王五', age=19}

这说明:

  • equals():比较内容
  • hashCode():生成一致的哈希值
  • toString():打印更友好的对象信息

六、初学者最容易踩的坑

1. 把 ==equals() 混为一谈

记住:

  • == 比较地址(引用类型)
  • equals() 比较内容(前提是重写了)

2. 只重写 equals(),不重写 hashCode()

这会导致哈希集合中行为异常。

3. 以为 toString() 不重要

实际上它对调试、打印日志、排查问题都很有帮助。

4. equals() 中直接调用属性的 equals(),可能空指针

比如:

name.equals(other.name)

如果 namenull,会报错。更推荐:

Objects.equals(name, other.name)

七、面试/考试常见问题总结

1. Object 类中最常用的方法有哪些?

答:equals()hashCode()toString()

2. equals() 和 == 有什么区别?

答:

  • == 对基本类型比较值,对引用类型比较地址
  • equals() 默认比较地址,重写后一般比较内容

3. 为什么重写 equals() 时通常要重写 hashCode()?

答:

因为 Java 规定相等对象必须有相同哈希值,否则在 HashSetHashMap 等集合中会出现逻辑问题。

4. 为什么要重写 toString()?

答:

为了让对象打印时显示更有意义的内容,方便调试和日志查看。


八、最后总结

学完 Object 类中的这三个方法后,你要真正记住下面这三句话:

  • equals():判断两个对象内容是否相等
  • hashCode():为对象提供哈希值,方便哈希集合快速定位
  • toString():返回对象的字符串表示,便于输出和调试

再进一步总结就是:

  • equals() 决定“逻辑上是否相等”
  • hashCode() 决定“哈希结构里怎么快速找”
  • toString() 决定“打印出来给人看是什么样子”

如果这三个方法你真正理解了,后面学习集合框架,尤其是 HashSetHashMap 时,会顺畅很多。


九、练习题

练习 1

定义一个 Book 类,包含 titleprice 两个属性,重写 equals()toString()

练习 2

继续完善 Book 类,再重写 hashCode(),并放入 HashSet 中测试是否能正确去重。

练习 3

分别使用 ==equals() 比较两个内容相同的字符串对象,观察输出结果并解释原因。

更多推荐