Bootstrap

Java 中的 Object、equals() 和 hashCode() 深度剖析

前言

Java 中的 Object 类是一个非常基础且重要的类,它是所有类的父类,所有的类(包括用户自定义的类)都是直接或间接继承自 Object 类。这意味着所有的类都可以使用 Object 类中定义的方法,这为我们编写更加通用和灵活的代码提供了基础。

今天,我们将深入探讨以下几个关键知识点:

  1. Object 类作为所有类的超类的作用和意义。

  2. equals() 方法的定义和使用,以及为什么需要重写 equals() 方法。

  3. hashCode() 方法的作用及其与 equals() 方法的关系,为什么在重写 equals() 方法时也必须重写 hashCode() 方法。

一、Object 类 —— 所有类的超类

Object 类是 Java 语言的基础类,它位于 java.lang 包中。所有的类,无论它们是直接定义的还是来自第三方库的,都继承自 Object 类。这就好像 Object 类是一个巨大的“容器”,囊括了所有 Java 对象的基本行为和属性。

Object 类中的常用方法

Object 类中定义了许多有用的方法,例如:

  • toString():返回对象的字符串表示形式。

  • equals(Object obj):用于比较两个对象是否相等。

  • hashCode():返回对象的哈希码。

  • clone():创建并返回对象的一个副本(需要实现 Cloneable 接口)。

  • getClass():返回对象的运行时类。

这些方法在默认情况下会有一些基本的行为,但通常情况下,我们可以通过重写这些方法来自定义它们的行为。

二、==equals() 的区别

在 Java 中,==equals() 都可以用来比较两个对象是否“相等”,但它们的含义和使用场景有所不同。

1. == 的含义和使用场景

  • 基本数据类型== 用于比较基本数据类型的值是否相等。例如:

    int a = 10;
    int b = 10;
    System.out.println(a == b); // 输出:true
  • 引用数据类型== 比较的是两个对象的引用是否指向同一个内存地址。例如:

    String s1 = new String("Hello");
    String s2 = new String("Hello");
    System.out.println(s1 == s2); // 输出:false

    这是因为 s1s2 是两个不同的对象,它们的内存地址不同。

2. equals() 的含义和使用场景

  • 基本数据类型equals() 方法不能用于比较基本数据类型的值。例如:

    int a = 10;
    int b = 10;
    System.out.println(a.equals(b)); // 编译错误,基本数据类型没有 equals() 方法
  • 引用数据类型equals() 方法用于比较两个对象的内容是否相等。它是 Object 类中的一个方法,默认实现与 == 类似,比较的是对象的引用。例如:

    String s1 = new String("Hello");
    String s2 = new String("Hello");
    System.out.println(s1.equals(s2)); // 输出:true

    这是因为 String 类重写了 equals() 方法,比较的是字符串的内容而不是引用。

3. 总结

  • ==:比较的是基本数据类型的值或者引用数据类型的引用。

  • equals():比较的是引用数据类型的内容(前提是该类重写了 equals() 方法)。

4. 示例代码

public class EqualsVsEquality {
    public static void main(String[] args) {
        // 基本数据类型
        int a = 10;
        int b = 10;
        System.out.println(a == b); // true

        // 引用数据类型
        String s1 = new String("Hello");
        String s2 = new String("Hello");
        System.out.println(s1 == s2); // false
        System.out.println(s1.equals(s2)); // true

        // 自定义类
        Cat cat1 = new Cat("小花");
        Cat cat2 = new Cat("小花");
        System.out.println(cat1 == cat2); // false
        System.out.println(cat1.equals(cat2)); // false(默认实现)
    }
}

class Cat {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    // 重写 equals 方法
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Cat other = (Cat) obj;
        return name != null ? name.equals(other.name) : other.name == null;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                '}';
    }
}

5. 运行结果

true
false
true
false
false

可以看到,当 Cat 类没有重写 equals() 方法时,cat1.equals(cat2) 的结果是 false,因为默认的 equals() 方法比较的是引用。只有重写 equals() 方法后,才能正确比较对象的内容。

三、equals() 方法的重写

equals() 方法的默认实现(来自 Object 类)是比较两个对象的引用,即是否指向同一个内存地址。如果两个对象是同一个实例,那么它们被认为是“相等”的;否则,即使它们的内容相同,也被视为不相等。

例如,考虑以下代码:

String a = "123";
String b = "123";
System.out.println(a.equals(b)); // true
System.out.println(a == b); // true

这里,ab 实际上是同一个 String 对象的引用,因此无论是 equals() 还是 ==,结果都是 true

但是,如果我们将字符串手动创建为 new String("123")

String a = new String("123");
String b = new String("123");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false

此时,ab 是两个不同的对象,== 返回 false,而 equals() 返回 true,因为 String 类重写了 equals() 方法,用于比较字符串的内容而非引用。

为什么需要重写 equals() 方法?

对于自定义类来说,如果默认的 equals() 方法基于引用比较,那么即使两个对象的内容完全相同,它们也会被视为不相等。例如,假设我们有一个 Dog 类:

public class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

如果我们创建两个 Dog 对象:

Dog dog1 = new Dog("小花");
Dog dog2 = new Dog("小花");
System.out.println(dog1.equals(dog2)); // 输出:false

默认情况下,dog1dog2 被视为不相等,因为它们是两个不同的对象(不同的内存地址)。这显然不符合我们的预期,因此我们需要重写 equals() 方法,使相同内容的对象被视为相等。

如何正确重写 equals() 方法

下面是一个重写 equals() 方法的示例:

public class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    // 重写 equals 方法
    @Override
    public boolean equals(Object obj) {
        // 如果 obj 为 null,直接返回 false
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        // 检查是否是同一个类型
        if (getClass() != obj.getClass()) {
            return false;
        }
        // 转换为当前类
        Dog other = (Dog) obj;
        // 比较属性
        if (name == null) {
            return other.name == null;
        } else {
            return name.equals(other.name);
        }
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

在这个示例中,我们首先检查 obj 是否为 null,然后检查是否是同一个对象(this == obj),接着检查是否是同一个类型。最后比较对象的属性(name)。

四、hashCode() 方法及其与 equals() 方法的关系

hashCode() 方法是一个 native 方法,它返回一个对象的哈希码,哈希码是一个整数(int 类型)。哈希码主要用于在哈希表(如 HashMapHashSet 等)中快速定位对象。

hashCode() 的默认实现

Object 类中 hashCode() 的默认实现是基于对象的内存地址生成的。这意味着不同的对象(即使内容相同)会有不同的哈希码。例如:

Dog dog1 = new Dog("小花");
Dog dog2 = new Dog("小花");
System.out.println(dog1.hashCode()); // 输出:随机整数
System.out.println(dog2.hashCode()); // 输出:另一个随机整数

为什么在重写 equals() 时必须重写 hashCode()

hashCode() 的核心设计原则之一是:如果两个对象通过 equals() 方法被认为是相等的,那么它们的 hashCode() 必须返回相同的值。反之,如果两个对象的 hashCode() 相同,它们并不一定相等。

原因在于,很多基于哈希表的数据结构(如 HashMapHashSet)依赖于 hashCode() 来快速定位对象所在的桶(bucket)。如果 equals()hashCode() 的行为不一致,那么可能会导致这些数据结构出现逻辑问题。

总的来说,就是使用hashcode方法时候我们一般是在hash表中进行定位和查询对象,如果重写了equals()方法就会改变原来判断对象是否相等的逻辑,从判断地址是否相等到判断值是否相等,就会导致hashcode()判断相等的对象,equals()判断不相等,或者反过来,所以之所以子类重写equals()方法的时候也要重写hashcode()就是要保证他们两个判断对象是否相等的逻辑是一样的。

如图所示,在哈希表中,多个对象可能被映射到同一个下标位置(例如下标为 5 的位置)。这些对象的 hashCode() 值相同,但它们的内容可能不同。如果我们在存储或查询这些对象时,仅依赖 hashCode() 来判断对象是否相等,而没有正确重写 equals() 方法,就会导致逻辑错误。

例如,假设我们有一个自定义的类 Cat,它重写了 equals() 方法,但没有重写 hashCode() 方法:

public class Cat {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        // 假设其他代码与 Dog 类相似
        return false; // 示意代码,实际应正确判断
    }
}

如果我们将两个相等的 Cat 对象放入 HashSet 中:

Cat cat1 = new Cat("小猫");
Cat cat2 = new Cat("小猫");
HashSet<Cat> set = new HashSet<>();
set.add(cat1);
set.add(cat2);
System.out.println(set.size()); // 可能返回 2,但应返回 1

由于 Cat 类没有重写 hashCode()cat1cat2 的哈希码不同,即使它们的内容相同。因此,HashSet 认为它们是不同的对象,导致集合的大小为 2,这显然不符合预期。

五、总结

通过本文的介绍,我们深入了解了 Java 中的 Object 类、equals() 方法和 hashCode() 方法的核心知识点。

  • Object 类是所有类的父类,提供了基本的方法。

  • equals() 方法用于定义对象相等的逻辑,可以通过重写来自定义比较行为。

  • hashCode() 方法与 equals() 方法紧密相关,只有同时重写这两者,才能保证对象在哈希表等数据结构中的正确性。

希望本文对你理解这些 Java 基础知识有所帮助!如果在实际项目中有任何疑问或应用场景,欢迎在评论区留言。

;