HashCode 算法为什么采用 31 作为乘数

哈希码与equals方法的重写以及31作为乘数的原理
本文详细解释了为何在Java中,当重写equals()方法时,也需要重写hashCode()。重写hashCode()是为了提高效率,减少equals比较的次数,尤其是在集合操作中。同时,文章探讨了为何选择31作为hashCode算法的乘数,原因是31是一个适中的质数,能平衡哈希冲突和性能。此外,31可以通过位运算优化,避免溢出并提高计算效率。

一、为什么String 重写 equals() 后还需要重写 hashcode()

hashCode() 源码:

两个对象比较,由于 Java 默认比较的是类的地址值,每个对象一定是不同的,所以重写 hashCode() 和 equals()。重写 hashCode() 是为了提高效率,因为先根据类里的属性生成 hashCode,如果生成的 hashCode 值不同,则没必要再使用 equals() 比较属性的值,这样就大大减少了 equals 的次数,这对大数据量比较的效率提高是很明显的。一个很好的例子就是在集合中的使用:

Java 中的 List 是有序的,因此可以重复。set 是无序的,因此不能重复的。那么如何保证 set 不能被放入重复的元素呢?假使原来集合中已有 10000 个元素了,单靠 equals() 比较的话,那么放入 10001 个元素,难道要将前面的所有元素都进行比较,看看是否有重复?这个效率可想而知,因此 hashcode 就应遇而生了。Java 就采用了 hash 表,利用哈希算法(也叫散列算法),就是将对象数据根据该对象的特征使用特定的算法将其定义到一个地址上,那么在后面定义进来的数据只要看对应的 hashCode 地址上是否有值,有就用 equals 比较,没有则直接插入,如此大大减少了 equals 的使用次数,执行效率就大大提高了。

为什么必须要重写 hashCode(),简言之就是为了保证同一个对象,在 equals 相同的情况下 hashCode 值也相同。如果重写了 equals() 而未重写 hashCode(),可能就会出现两个没有关系的对象 equals 相同(因为 equals 都是根据对象的特征进行重写的),但 hashCode 不同的情况。

String 是遵守类 hashCode 的协定,equals() 相同,如何保持 hashCode 相同。初始 String 对象成员变量 int hash 默认是 0,调用 hashCode() 之后h = 31 * h + val[i],其实就是 String 内部根据 char[] 来进行一个运算。那么只要内容 equals(),其内容运算生产的 hashCode() 也是相等的。这里也阐明了 equals() 与 hashCode() 在一般应用中的正向关系。

二、HashCode 算法为什么采用 31 作为乘数

1️⃣更少的乘积结果冲突
31 是质子数中一个“不大不小”的存在,如果使用的是一个如 2 的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个 100 以上的质数,得出的哈希值会超出int 的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hashCode() 运算,并使用常数 31、33、37、39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于 7 个(国外大神做的测试),那么这几个数就被作为生成 hashCode 值的备选乘数了。

2️⃣31 可以被JVM 优化

位运算是 JVM 里最有效的计算方式:

  1. 【左移 <<】左边的最高位丢弃,右边补全0(把 << 左边的数据*2的移动次幂)。
  2. 【右移 >>】把>>左边的数据/2的移动次幂。
  3. 【无符号右移 >>>】无论最高位是 0 还是 1,左边补齐 0。

所以:31 * i = (i << 5) - i【例如:31*2=62 转换为 2*2^5-2=62】

3️⃣理解

  1. 首先 hash 函数必须要选用质/素数,这个是被科学家论证过的 hash 函数减少冲突的一个理论。
  2. 如果设置为偶数的话会存在溢出的情况,导致信息丢失(因为使用偶数相当于使用了移位运算)。
  3. 可以兼顾到虚拟机的性能,虚拟机默认使用2<<5-1来得到很好的性能,且其是一个不大不小的质数,兼顾了性能和冲突率。

为什么 31 的性能和解决冲突是最优的?回顾 String 对象的 hashCode():

/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

选择不大不小的质数的原因

正如备注上所描述的那样,这段代码的实际执行方法如下:

s[0]*31^(n-1) + s[1]*31^(n-2) ++ s[n-1] 推算过程为
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]

因此可以代入式的计算一下,假如有一个String test = “abcdef”六个字母。使用一个较小的质数 2 或者一个较大的质数 101 带入进去,前者2^(6-1) = 32,后者101^(6-1) = 10 510 100 501。 一个太小一个太大。太小的话 hash 冲突的概率就会相对大一些,太大的话就会太过于分散,对于 hash 结构的集合来说就会占用过的存储空间。因此选择不大不小的质数是非常有必要的。

如何证明 31 这个数值就比其他数值优呢?

整型的数值区间是 [-2147483648, 2147483647],区间大小为 2^32。所以这里可以将区间等分成 64 个子区间,每个子区间大小为 2^26。

  1. 乘子 2 算出的哈希值几乎全部落在第 32 分区,也就是 [0, 67108864) 数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。这也就不难解释为什么数字 2 作为乘子时,算出哈希值的冲突率如此之高的原因了。

  2. 乘子 101,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。但是还是因为乘积太大导致整数溢出的问题。所以 Java 在设计的时候是为了兼顾性能和 hash 函数的分散性。

由此,使用 31 这数值,可以说是最佳实践。

<think>好的,我需要详细解释Java中的toString()、equals()hashCode()方法。首先,我得回顾一下每个方法的基本作用和默认实现。 toString()方法通常用于返回对象的字符串表示。默认情况下,它返回类名和哈希码,但用户通常会重写它以便更直观地显示对象内容。例如,String类重写了toString(),直接返回字符串本身。 接下来是equals()方法。默认的equals()在Object类中使用的是==比较,即检查两个引用是否指向同一个对象。但在实际应用中,比如比较两个内容相同的不同字符串对象时,需要重写equals()来比较对象的内容而不是引用。例如,String类的equals()被重写为逐个字符比较。 然后是hashCode()方法,它返回对象的哈希码。根据Java规范,如果两个对象通过equals()方法相等,它们的hashCode()必须相同。因此,重写equals()时必须同时重写hashCode(),以确保一致性。例如,String类的hashCode()根据字符串内容计算哈希值,保证相同字符串有相同的哈希码。 用户提供的引用中有提到,String的hashCode()实现使用31作为乘数,这可能是因为31是一个奇素数,有助于减少哈希冲突。同时,引用中还指出不同值的对象可能有相同的哈希码,比如例子中的"Aa"和"BB",这说明即使hashCode()相同,equals()也可能返回false,所以不能单独依赖hashCode()判断相等。 现在需要考虑如何将这些信息组织成结构化的回答,涵盖方法的作用、默认实现、重写必要性及示例。同时,要确保引用的正确使用,比如引用[^1]提到hashCode是一种算法,引用[^2]强调equals和hashCode必须同时重写,引用[^3]给出哈希冲突的例子,引用[^4]解释String的hashCode实现。 可能的问题点:是否需要解释如何正确重写这些方法?比如在重写equals时要检查null、类型匹配等。此外,要强调当使用基于哈希的集合(如HashMap)时,正确的hashCode和equals实现的重要性,否则可能导致无法正确检索对象。 最后,生成相关问题需要考虑用户可能的后续疑问,比如重写方法的步骤、哈希冲突的影响、不重写的后果等。</think>### Java中toString()、equals()hashCode()方法详解 #### 一、toString()方法 1. **作用** 返回对象的字符串表示形式,默认实现为`类名@十六进制哈希码`。主要用于调试和日志输出。 2. **默认实现** ```java public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } ``` 3. **重写场景** 需展示对象内部状态时,例如: ```java public class Person { private String name; private int age; @Override public String toString() { return "Person{name='" + name + "', age=" + age + "}"; } } ``` --- #### 二、equals()方法 1. **作用** 判断两个对象是否“逻辑相等”,默认实现为`==`比较(即对象地址相同)[^2]。 2. **默认实现** ```java public boolean equals(Object obj) { return (this == obj); } ``` 3. **重写规则** - **自反性**:`x.equals(x)`必须为`true` - **对称性**:若`x.equals(y)`为`true`,则`y.equals(x)`也需为`true` - **传递性**:若`x.equals(y)`和`y.equals(z)`为`true`,则`x.equals(z)`也需为`true` - **一致性**:多次调用结果应相同 - **非空性**:`x.equals(null)`必须为`false` 4. **示例(重写)** ```java public class Person { @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } } ``` --- #### 三、hashCode()方法 1. **作用** 返回对象的哈希码,用于哈希表(如`HashMap`、`HashSet`)的快速查找。若两个对象通过`equals()`相等,其哈希码必须相同[^4]。 2. **默认实现** 基于对象地址生成哈希值(与JVM实现相关)。 3. **重写原则** - 若`equals()`重写,则`hashCode()`必须同步重写。 - 不同对象的哈希码允许相同(哈希冲突),但应尽量减少[^3]。 4. **示例(String类的实现)** ```java public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } ``` 采用31作为乘数以减少冲突,且满足`"Aa"`与`"BB"`哈希码相同的情况[^3]。 --- #### 四、三者关系总结 1. **equals()hashCode()的绑定** 若两个对象`equals()`结果为`true`,其`hashCode()`必须相同;反之不成立[^2][^4]。 2. **toString()的辅助作用** 通过重写`toString()`可直观验证对象状态,辅助调试`equals()`和`hashCode()`的逻辑。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值