java 中的 hashCode 方法

本贴最后更新于 2280 天前,其中的信息可能已经时移世异

1. hashCode()方法定义

java 中 Object 类的位数不多的方法中有一个 hashCode 方法:

public native int hashCode();

根据源码中方法的定义和注释,我们简单总结下:

  • 该方法返回 int 类型数据,并且是本地方法。(源于本地方法请参考我转载的文章:https://www.jianshu.com/p/17a0ae232687)
  • 在一次 java 应用执行中,对于同一个对象,hashCode 方法必须返回相同的整数,前提是通过 equals 方法比较认为此对象没有被修改。并且同一应用的不同执行时,hashCode 值不必保持一致。
  • 如果两个对象根据 equals 方法相等,那么这俩对象调用 hashCode 方法返回的证书结果也想同。
  • 如果两个对象调用 equals 方法不相等,但是调用 hashCode 方法不一定会产生两个不同的结果。
    但是最为我们程序猿来说应该意识到对于不同对象产生不同的 hashCode 是会提高哈希表的性能的。

2. hashCode()方法的作用

在 java 中作为所有类的父类的 Object 类能够有 hashCode()这个方法,就说明了此方法有着很大的作用。那我们来看一下 hashCode()方法的作用吧。

  • hashCode 的主要作用是为了配合基于散列的集合一起正常运行,如 HashSet, HashMap, HashTable
    大家都知道 set 的值不能重复,map 的 key 不能重复。那么如何判断在集合中已经存在呢?
    大多数人可能会想到用 equals 比较,这个方法确实可行,但是逐个比较的话成本太高。如果集合中数据很多的话,效率必然会成为瓶颈。此时 hashCode 方法的出现就完美的解决了这个问题。

当集合要添加新的对象时,先调用这个对象的 hashCode 方法,得到对应的 hashcode 值,实际上在 HashMap 的具体实现中会用一个 table 保存已经存进去的对象的 hashcode 值,如果 table 中没有该 hashcode 值,它就可以直接存进去,不用再进行任何比较了;如果存在该 hashcode 值, 就调用它的 equals 方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用 equals 方法的次数就大大降低了,说通俗一点:Java 中的 hashCode 方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。下面这段代码是 java.util.HashMap 的中 put 方法的具体实现:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
 
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put 方法是用来向 HashMap 中添加新的元素,从 put 方法的具体实现可知,会先调用 hashCode 方法得到该元素的 hashCode 值,然后查看 table 中是否存在该 hashCode 值,如果存在则调用 equals 方法重新确定是否存在该元素,如果存在,则更新 value 值,否则将新的元素添加到 HashMap 中。从这里可以看出,hashCode 方法的存在是为了减少 equals 方法的调用次数,从而提高程序效率。

3. 在重写 equals 方法的同时,必须重写 hashCode 方法

具体原因笔者这会儿有点懒不赘述啦, 另外有两篇写得比较好的文章推荐一下:

参考文章 1:http://bbs.itheima.com/forum.php?mod=viewthread&tid=234627&page=1
参考文章 2:https://www.cnblogs.com/dolphin0520/p/3681042.html#top

4. Hash 函数的设计

Hash 函数设计的好坏直接影响到对 Hash 表的操作效率。下面举例说明:

  假如对上述的联系人信息进行存储时,采用的 Hash 函数为:姓名的每个字的拼音开头大写字母的 ASCII 码之和。

  因此 address(张三)=ASCII(Z)+ASCII(S)=90+83=173;

    address(李四)=ASCII(L)+ASCII(S)=76+83=159;

    address(王五)=ASCII(W)+ASCII(W)=87+87=174;

    address(张帅)=ASCII(Z)+ASCII(S)=90+83=173;

  假如只有这 4 个联系人信息需要进行存储,这个 Hash 函数设计的很糟糕。首先,它浪费了大量的存储空间,假如采用 char 型数组存储联系人信息的话,则至少需要开辟 174*12 字节的空间,空间利用率只有 4/174,不到 5%;另外,根据 Hash 函数计算结果之后,address(张三)和 address(李四)具有相同的地址,这种现象称作冲突,对于 174 个存储空间中只需要存储 4 条记录就发生了冲突,这样的 Hash 函数设计是很不合理的。所以在构造 Hash 函数时应尽量考虑关键字的分布特点来设计函数使得 Hash 地址随机均匀地分布在整个地址空间当中。通常有以下几种构造 Hash 函数的方法:

  1)直接定址法

  取关键字或者关键字的某个线性函数为 Hash 地址,即 address(key)=a*key+b;如知道学生的学号从 2000 开始,最大为 4000,则可以将 address(key)=key-2000 作为 Hash 地址。

  2)平方取中法

  对关键字进行平方运算,然后取结果的中间几位作为 Hash 地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为 Hash 地址。

  3)折叠法

  将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成 Hash 地址。假如知道图书的 ISBN 号为 8903-241-23,可以将 address(key)=89+03+24+12+3 作为 Hash 地址。

  4)除留取余法

  如果知道 Hash 表的最大长度为 m,可以取不大于 m 的最大质数 p,然后对关键字进行取余运算,address(key)=key%p。

  在这里 p 的选取非常关键,p 选择的好的话,能够最大程度地减少冲突,p 一般取不大于 m 的最大质数。

作者 @ 没有故事的老大爷
一个人至少拥有一个梦想,有一个理由去坚强。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...