从语言设计的角度探究Java中hashCode()和equals()的关系

一. 基础: hashCode()和equals()简介

在学习hashCode()和equals()之间的关系以前, 咱们有必要先单独了解他俩自身的特色.java

  • equals()方法用于比较两个对象是否相等, 它与"=="相等比较符有着本质的不一样. 在万物皆对象的Java体系中, 系统把判断对象是否相等的权力交给程序员, 具体的措施是把equals()方法写入到Object类中, 并让全部类继承Object类. 这样程序员就能在类中自定义equals()方法, 从而实现本身的业务逻辑.
  • 关于equals()和"=="的区别你能够--参考这篇文章--

 

  • hashCode()的意思是哈希值, 哈希值是哈希函数运算后获得的结果, 哈希函数可以保证相同的输入可以获得相同的输出(哈希值), 可是不可以保证不一样的输入老是能得出不一样的输出. 当输入的样本量足够大时, 是会产生哈希冲突的, 也就是不一样的输入产生了相同的输出.
  • 暂且不谈冲突, 就相同的输入可以产生相同的输出这点而言, 是及其宝贵的. 它使得系统只须要经过简单的运算, 在时间复杂度O(1)的状况下就能得出数据的映射关系, 根据这种特性引伸出了散列表这种数据结构.
  • 一种主流的散列表实现是: 用数组做为哈希函数的输出域, 输入值通过哈希函数计算后获得哈希值, 而后根据哈希值在数组种找到对应的存储单元. 当发生冲突时, 对应的存储单元以链表的形式保存冲突的数据.

 

二. 漫谈: 引入hashCode()与equals()之间的关系

下面咱们从一个宏观的角度引入hashCode()和equals()之间的关系程序员

  • 在大多数编程实践中, 归根结底会落实到数据的存取问题上. 在汇编语言时代, 你须要老老实实地对每一个数据操做编写存取语句; 随着时代发展到今天, 咱们都用相似Java这样的高级语言编写代码. Java除了拥有面向对象的核心思想外, 还给咱们封装了一系列操做数据的api, 为编程工做提供了极大的便利.
  • 但在咱们对数据进行操做以前, 首先要把数据按照必定的数据结构保存到存储单元中, 不然操做数据将无从谈起. 然而不一样的数据结构有各自的特色, 咱们在存储数据的时候须要选择适合本身的数据结构进行存储. Java根据不一样的数据结构提供了丰富的容器类, 方便程序员选择适合业务的容器类进行开发.
  • 而Java的容器类被分为Collection和Map两大类, Collection又能够进一步分为List和Set. 其中Map和Set都是不容许元素重复的, 严格来讲Map存储的是键值对, 它不容许重复的键值. 值得注意的是: Map和Set的绝大多数实现类的底层都会用到散列表结构.
  • 讲到这里咱们提取两个关键字不容许重复散列表结构, 回顾hashCode()和equals()的特色, 你是否想到了些什么东西呢?

 

三. 解密: 深刻理解hashCode()和equals()之间的关系.

  • 上面提到Set和Map不存放重复的元素(key), 那么在存储元素的时候就必须对元素作出判断: 在当前的容器中有没有和新元素相同的元素?.
  • 你可能会想: 这容易呀, 直接调用元素对象的equals()方法进行比较不就好了吗? 若是容器中的存储的对象数量较少, 这确实是个好主意, 可是若是容器中存放的对象达到了必定的规模, 要调用容器中全部对象的equals()方法和新元素进行比较就不是一件容易的事情了, 就算equals()方法的比较逻辑简单无比, 这也是一个时间复杂度为O(n)的操做啊.

 

  • 但在散列表的基础上判断"新对象是否和容器中任一对象相同"就容易得多了. 因为每一个对象都自带有hashCode(), 这个hashCode将会用做散列表哈希函数的输入, hashCode通过哈希函数计算后获得哈希值, 新对象根据哈希值存储到相应的内存的单元.
  • 咱们不妨假设两个相同的对象, hashCode()必定相同, 这么一来就体现出哈希函数的威力了, 因为相同的输入必定会产生相同的输出, 因而若是新对象和容器中已存在的对象相同, 新对象计算出的哈希值就会和已存在的对象的哈希值产生冲突, 这时容器就能判断: 这个新加入的元素已经存在, 须要另做处理(覆盖掉原来的元素(key)或舍弃).
  • 按照这个思路, 若是这个元素计算出的哈希值所对应的地址没有产生冲突, 也就是没有重复的元素, 那么它就能够直接插入. 因此当运用hashCode()时, 判断是否有相同元素的代价只是一次哈希计算, 时间复杂度为O(1), 这极大地提升了数据的存储性能.

 

  • 可是前面咱们还提到: 当输入样本量足够大时, 不相同的输入是会产生相同输出的, 也就是造成哈希冲突. 这么一来就麻烦了, 原来咱们设定的"若是产生冲突, 就意味着两个对象相同"的规则瞬间被打破, 产生冲突的颇有多是两个不一样的对象!
  • 而使人欣慰的是咱们除了hashCode()方法, 还有一张王牌: equals()方法. 也就是说当两个不相同的对象产生哈希冲突后, 咱们能够用equals()方法进一步判断两个对象是否相同. 这时equals()方法就至关重要了, 这个状况下它必需要能断定这两个对象是不相同的.
  • 讲到这里就引出了Java程序设计中一些重要原则:
  • 若是两个对象是相等的, 它们的equals()方法应该要返回true, 它们的hashCode()须要返回相同的结果.
  • 但有时候面试题不会问得这么直接, 它会问你:两个对象的hashCdoe()相同, 它的equals()方法必定要返回true, 对吗?
  • 那答案确定不对. 由于咱们不能保证每一个程序设计者都会遵循编码规则, 有可能两个不一样对象的hashCode()会返回相同的结果. 若是你理解上面的内容, 这个问题就很好解答: 两个对象的hashCode()相同, 未来会在散列表中产生哈希冲突, 可是它们不必定是相同的对象呀. 当产生哈希冲突时, 咱们还得经过equals()方法进一步判断两个对象是否相同, equals()方法不必定会返回true.
  • 这也是为何Java官方推荐咱们最好同时重写hashCode()和equals()方法的缘由.

 

四. 验证: 结合HashMap的源码和官方文档, 验证二者的关系.

以上的文字是我通过思考后得出的, 它有必定依据但并不是彻底可靠, 下面咱们根据HashMap的源码(JDK1.8)和官方文档来验证这些推论是否正确.面试

  • 经过阅读JDK8的官方文档, 咱们发现equals()方法介绍的最后有这么一段话:

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.编程

  • 官方文档提醒咱们当重写equals方法的时候, 最好也要重写hashCode()方法. 也就是说若是咱们经过重写equals方法判断两个对象相同时, 他们的hash code也应该相同, 这样才能让hashCode()方法发挥它的做用.
  • 那它究竟能发会怎样的做用呢? 咱们结合部分较为经常使用的HashMap源码进一步分析. (像HashSet底层也是经过HashMap实现)
  • 在HashMap中用得最多无疑是put()方法了, 如下是put()的源码:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
复制代码
  • 咱们能够看到put()方法实际调用的是putVal()方法, 继续跟进:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //在咱们建立HashMap对象的时候, 内存中并无为HashMap分配表的空间, 直到往HashMap中put添加元素的时候才调用resize()方法初始化表
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//同时肯定了表的长度
        
    //((n - 1) & hash)肯定了要put的元素的位置, 若是要插入的地方是空的, 就能够直接插入.
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//若是发生了冲突, 就要在冲突位置的链表末尾插入元素
        Node<K,V> e; K k;
        if (p.hash == hash &&   
            ((k = p.key) == key || (key != null && key.equals(k))))
            //关键!!!当判断新加入的元素是否与已有的元素相同, 首先判断的是hash值, 后面再调用equals()方法. 若是hash值不一样是直接跳过的
            e = p;
        else if (p instanceof TreeNode)//若是冲突解决方案已经变成红黑树的话, 按红黑树的策略添加结点. 
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//解决冲突的方式还是链表
            for (int binCount = 0; ; ++binCount) {//找到链表的末尾, 插入.
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);//插入以后要判断链表的长度, 若是到达必定的值就可能要转换为红黑树. 
                    break;
                }//在遍历的过程当中仍会不停地断定当前key是否与传入的key相同, 判断的第一条件仍然是hash值. 
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;//修改map的次数增长
    if (++size > threshold)//若是hashMap的容量到达了必定值就要进行扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}
复制代码
  • 咱们能够看到每当判断key是否相同的是否, 首先会判断hash值, 若是hash值相同(产生了冲突), 而后会判断key引用所指的对象是否相同, 最终会经过equals()方法做最后的断定.
  • 若是key的hash值不一样, 后面的判断将不会执行, 直接认定两个对象不相同.
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
复制代码

结束api

  • 讲到这里但愿你们对hashCode()与equals()方法能有更深刻的理解, 明白背后的设计思想与原理.
  • 我以前有一个疑问, 可能你们看完这篇文章后也会有: equals()方法平时我会用到, 因此我知道它除了和hashCode()方法有密切联系外, 还有别的用途. 可是hashCode()呢, 它除了和equals()方法有密切联系外, 还有其余用途吗?
  • 通过在互联网上一番搜寻, 我目前给出的答案是没有. 也就是说hashCode()仅在散列表中才有用,在其它状况下没用.
  • 固然若是这个答案不正确, 或者你还有别的思考, 欢迎留言与我交流~
相关文章
相关标签/搜索