java中的hashCode方法

  Seves

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的最大质数。

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

439