杰瑞科技汇

Java和hashCode有什么关联?

运算符

是一个关系运算符,用于比较两个变量(或表达式)的值是否相等,它的行为取决于你比较的是什么类型的变量。

Java和hashCode有什么关联?-图1
(图片来源网络,侵删)

比较基本数据类型(Primitive Types)

当 用于比较基本数据类型(如 int, double, char, boolean 等)时,它会比较它们的是否相等。

int a = 10;
int b = 10;
int c = 20;
System.out.println(a == b); // 输出: true
System.out.println(a == c); // 输出: false

对于基本类型, 比的是“值是否相等”。

比较引用数据类型(Reference Types)

当 用于比较引用数据类型(如所有自定义的类、StringArray 等)时,它会比较这两个引用是否指向同一个内存对象(即它们的内存地址是否相同),这被称为“浅比较”或“引用比较”。

class Person {
    String name;
    Person(String name) {
        this.name = name;
    }
}
public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice");
        Person p2 = new Person("Alice");
        Person p3 = p1;
        System.out.println(p1 == p2); // 输出: false
        // 解释:p1和p2是两个不同的对象,它们在内存中的地址不同。
        System.out.println(p1 == p3); // 输出: true
        // 解释:p3被赋值为p1的引用,所以它们指向同一个内存地址。
    }
}

对于引用类型, 比的是“两个引用是否指向同一个对象(地址是否相同)”。

Java和hashCode有什么关联?-图2
(图片来源网络,侵删)

hashCode() 方法

hashCode()Object 类中的一个方法,它的主要目的是为了提高哈希表(如 HashMap, HashSet, Hashtable)的存取效率

  • 返回值:一个 int 类型的整数,被称为“哈希码”或“散列码”。
  • 目的:它为对象在内存中生成一个相对唯一的“数字指纹”,这个指纹被哈希表用来确定对象应该被存放在哪个“桶”(bucket)中。

hashCode() 的约定(来自 Object 类的 JavaDoc)

为了确保哈希表(如 HashMap)的正确性和高效性,Java 规范要求任何重写了 equals() 方法的类,也必须重写 hashCode() 方法,并遵守以下三条约定:

  1. 一致性:在程序运行期间,只要对象的 equals() 比较所用的信息没有被修改,那么对该对象多次调用 hashCode() 方法,必须 consistently 返回同一个整数。一次执行到另一次执行,这个整数可以不同。
  2. 相等的对象必须具有相同的哈希码a.equals(b)truea.hashCode() 必须等于 b.hashCode()(这是最重要的一条!)
  3. 不相等的对象,其哈希码不要求必须不同a.equals(b)falsea.hashCode()b.hashCode() 不要求一定不相等。开发时应尽量让不相等的对象产生不同的哈希码,这能提高哈希表的性能(减少哈希冲突)。

hashCode() 的核心区别与联系

特性 运算符 hashCode() 方法
类型 运算符 方法
基本类型:值是否相等。
引用类型:内存地址是否相同。
返回一个对象的哈希码(一个整数)。
目的 判断两个变量在值或引用上是否相等。 为对象生成一个“数字指纹”,用于哈希表定位。
能否被重写 不能。 的行为是语言内置的,无法被重写。 可以,强烈建议在重写 equals() 时重写它。
默认行为 基本类型:比较值。
引用类型:比较内存地址。
返回对象的内存地址的某种形式(通常是 System.identityHashCode(this))。

为什么需要同时使用它们?—— HashMap 的工作原理

这是理解两者关系的关键,我们以 HashMap 为例,看看 hashCode()equals() 是如何协同工作的,以及 在其中扮演的角色。

假设我们要将一个自定义对象 MyObject 存入 HashMap

Java和hashCode有什么关联?-图3
(图片来源网络,侵删)
Map<MyObject, String> map = new HashMap<>();
MyObject key = new MyObject("some data");
map.put(key, "some value");

map.put(key, value) 的内部流程大致如下:

  1. 计算哈希码

    • HashMap 首先调用 key.hashCode() 方法,得到一个哈希码,12345
    • HashMap 内部会使用这个哈希码来计算出这个对象应该被存放在哪个“桶”(bucket)中。index = hash & (n-1)nHashMap 的容量。
  2. 检查桶中的元素

    • HashMap 会去计算出的那个“桶”中查找。
    • 桶是空的,直接将新的键值对 (key, value) 放入这个桶,结束。
    • 桶不为空(意味着发生了“哈希冲突”,多个对象的哈希码相同或映射到了同一个桶),这时就需要进一步比较了。
  3. 使用 equals() 进行精确比较

    • HashMap 会遍历这个桶中的所有元素,用新来的 key 去和桶里已有的每个 key 调用 equals() 方法进行比较。
    • equals() 返回 true:说明两个 key 在逻辑上是“相等”的。HashMap 会认为这是对同一个 key 的重复操作,它会用新的 value 覆盖掉旧的 value
    • equals() 返回 false:说明两个 key 逻辑上不相等,即使它们的哈希码相同(哈希冲突),HashMap 也会认为这是两个不同的键,它会在这个桶中创建一个新的链表或红黑树节点,将新的键值对挂载上去。

HashMap 中的角色:

HashMap 的源码中,当比较桶中的节点时,它首先会用 进行一次快速检查:

// 伪代码
if (p.key == newKey) { // 使用 == 进行快速比较
    // 找到完全相同的对象,直接覆盖
    p.value = newValue;
    return;
}
// 如果不是同一个对象,再调用 equals 进行逻辑比较
if (p.key.equals(newKey)) {
    // 逻辑上相等,覆盖
    p.value = newValue;
    return;
}

使用 是一个非常高效的优化,因为如果两个引用指向的是同一个对象,那么它们的 equals() 结果也必然是 true,先用 判断,可以避免调用 equals() 方法带来的额外开销。


最佳实践与常见陷阱

重写 equals() 时必须重写 hashCode()

这是一个硬性规定,违反它会导致严重问题。

错误示例:

class User {
    String name;
    // ... 构造函数, getter, setter
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }
    // 忘记重写 hashCode()!
}
public class Main {
    public static void main(String[] args) {
        Map<User, String> map = new HashMap<>();
        User user1 = new User("Alice");
        User user2 = new User("Alice");
        map.put(user1, "First User");
        System.out.println(map.get(user2)); // 输出: null !!!
    }
}

为什么输出 null

  1. map.put(user1, ...) 时,user1.hashCode() 被调用,假设返回 100user1 被存放在哈希码为 100 的桶中。
  2. map.get(user2) 时,user2.hashCode() 被调用,由于我们没有重写 hashCode(),它使用 Object 类的默认实现,这个实现基于对象的内存地址。user1user2 是两个不同的对象,所以它们的内存地址不同,导致 user2.hashCode() 返回的值(200)和 user1.hashCode() 不同。
  3. HashMap 直接去哈希码为 200 的桶中查找,当然找不到 user1,所以返回 null

即使 user1.equals(user2)true,但在 HashMap 看来,它们因为哈希码不同,就是两个完全不同的键。

如何正确地重写 hashCode()

手动编写既繁琐又容易出错,最佳实践是使用 IDE(如 IntelliJ IDEA 或 Eclipse)的自动生成功能,或者使用 Objects.hash() 工具类。

使用 Objects.hash() (推荐):

import java.util.Objects;
class User {
    String name;
    int id;
    // ... 构造函数, getter, setter
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id && Objects.equals(name, user.name);
    }
    @Override
    public int hashCode() {
        // IDE 生成或手动编写
        return Objects.hash(name, id);
    }
}

概念 核心要点
- 基本类型:比较
- 引用类型:比较内存地址(是否为同一个对象)。
- 不可重写,是语言内置的。
hashCode() - 是一个方法,返回一个 int 类型的哈希码。
- 主要用于哈希表(HashMap等),快速定位对象存储位置。
- 可以重写,重写 equals() 时必须重写它。
核心关系 - hashCode() 是为了性能,它快速缩小查找范围。
- equals() 是为了精确性,它确定两个对象在逻辑上是否相等。
- HashMap 依赖两者协同工作:先用 hashCode() 找到“桶”,再用 equals() 在桶里精确查找。
黄金法则 如果两个对象相等(a.equals(b)true),那么它们的哈希码必须相等(a.hashCode() == b.hashCode())。
(反过来不成立,哈希码相等的对象不一定相等。)

理解 和 hashCode() 的区别与联系,是迈向Java高级开发、掌握集合框架和对象设计的关键一步。

分享:
扫描分享到社交APP
上一篇
下一篇