Java:hashcode和equals

前言:

使用HashMap,HashSet等带有Hash字眼的的API时,如果存放自定义实例类,必须重写其hashcode()和equals()方法。这是为什么?

我们学习HashMap和HashSet时,知道有着"去重"的特性,想当然的认为在里面存放对象都能保证唯一性。实际上此类API操作对象的存与取时,总是需要使用hashcode()和equals()

1. 什么是hashcode呢?

  • hashcode是hash函数输入的映射值,java中的hashcode()函数就是利用对象内存地址的整数值做输入,计算得到的一个hashcode。
  • hashcode用于计算hashtable里映射对象的地址,也就是数组的下标,通过对象的hashcode可以直接得到对象的下标位置,从而提高查找对象的速度。
  • 所有类的基类Object拥有这个hashcode()方法,用于支持hash tables的API比如HashMap
  • 对于不同的对象,是可能发生hash collision从而产生相同的hashcode的,此时通过hashcode查询对象时可能会查到一堆对象,怎么办呢?这时候就需要该对象的equals()方法

2. 什么是equals呢?

  • equals(Object obj)是Object自带方法,对于所有不重写equals()的新类而言可以理解为是否指向同一地址的对象,此时同于==运算符。
  • 对于非指向null的对象,equals具有反射性,对称性,传递性和持久性。
  • equals的重写必须同时重写hashcode()方法,以保证hashcode()约定标准的一致性。
    这种一致性体现在:
    1. 在一个运行应用的时段内,同一个对象多次使用hashcode(), 返回值不变。
    2. equals()返回true,比较的对象信息不变的情况下,每次调用hashcode()对于此2个对象必须返回同值。
    3. equals()比较结果不同的2个对象,hashcode不要求不同,但需要注意碰撞对hash表性能的影响。

以上可以看出equals的4大特性都体现在hashcode接口标准上,两者联系紧密,必须同步重写。

3. 对象为什么要重写equals?

equals比较的是对象的内存首地址,应用的一次运行环境内,两个对象的实例如果是new构造的,那么不会返回true。我们也不应以地址为判断对象是否属于同一个的标准,实际应用中比较的应当是两者内部的属性。String中重写了equals,一个一个比较2个字符串数组中字符,就是最好的例子。又比如学生列表中插入student,student没有重写equals接口,如果2个student实例字段值都一致,存入前实现去重,你是不是去看一遍student源码然后一个个字段提取出来比较?这就说明student本身有重写equals的需求。而由之前的讨论可知,hashcode()也要重写来维护类支持hash table的一致性。

4. equals和hashcode接口运用实例 - HashMap中的put(k,v)接口 (java 8)

这里引用_Meng博主的源码分析,参考 https://www.cnblogs.com/mengchunchen/p/9239675.html

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
    // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
 
    else {// 数组该位置有数据
        Node<K,V> e; K k;
        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 到这里,说明数组该位置上是一个链表
            for (int binCount = 0; ; ++binCount) {
                // 插入到链表的最后面(Java7 是插入到链表的最前面)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在该链表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 说明存在旧值的key与要插入的key"相等"
        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

由上可知,HashMap的put接口计算得出key的下标,如果该下标指向非空,那么说明发生了碰撞,首先判断该位置存有的node.key是否和该key属于同一对象,如何判断同一对象呢,上面的代码是这样做的:

 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

如果hash值相同,并且两个key的引用地址相同或者满足非空对象equals返回true。put会直接覆盖新的value,返回旧的value。这句话体现了Object中对hashtable的约定,2个对象是同一对象,必须hash值相同,并且equals。所有的子类也应该遵守,否则无法适用hashtable的存储结构。

猜你喜欢

转载自blog.csdn.net/OliverZang/article/details/85006032