Java 中的 hashCode 和 equals 方法之间有什么关系?


Java 中的 hashCode 和 equals 方法之间有什么关系?

hashCode() 是属于 Object 的一个方法,并且是个 native 方法,本质就是返回一个哈希码,即一个 int 值,一般是一个对象的内存地址转成的整数。

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

equals,我们知道是用来判断两个对象是否相同的,也是属于 Object 的一个方法,并且默认实现如下:

public boolean equals(Object obj) {
        return (this == obj);
    }

单纯从两个方法的源码上看,是不是觉得 hashCode 和 equals 没啥关系?那为什么要放在一起说?

确实,一般情况下两者是没啥关系。但,如果是将一个对象在散列表相关类的时候,是有关系的。

比如 HashSet,我们常用来得到一个不重复的集合。

现在有个 User 类的 HashSet 集合,我只重写了 User 类的 equals 方法,表明如果 name 相同就返回 true。

public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj instanceof User	) {
        User other = (User) obj;
        return name.equals(other.name);
    }
    return false;
}

就重写一个 equals 的话,HashSet 中会出现相同 name 的 User 对象。

原因就是 hashCode 没有重写,那为什么会这样呢?因为 HashSet 是复用 HashMap 的能力存储对象,而塞入 key 的时候要计算 hash 值,可以看到这里实际会调用对象的 hashCode 方法来计算 hash 值。

        Set<User> userSet = new HashSet<>();
        User user = new User();
        user.setId(1L);
        user.setUserAccount("");
        user.setUserPassword("");
        user.setUnionId("");
        user.setMpOpenId("");
        user.setUserName("");
        user.setUserAvatar("");
        user.setUserProfile("");
        user.setUserRole("");
        user.setCreateTime(new Date());
        user.setUpdateTime(new Date());
        user.setIsDelete(0);

        userSet.add(user);

HashSet的add()方法重新实现了 Set中的方法:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

map.put()是调用的 HashMap 类中的 put()方法,该方法中调用了 putVal(), putVal()中的第一个入参是-调用了一个 hash() 方法计算出传入 key 的 hash 值,然后在内部执行putVal:

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

    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }

然后在具体执行 putVal 方法的时候,相关的判断条件会先判断 hash 值是否相等,如果 hash 值都不同,那就认为这两个对象不相等,这与我们之前设定的 name 一样的对象就是相等的条件就冲突了,我们简单看下源码就清楚了:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            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))))
                    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;
                        }
                        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;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }

可以看到,相关的判断条件都是先判断 hash 值,如果 hash 值相等,才会接着判断 equals。如果 hash 值不等,这个判断条件直接就 false 了。

因此规定重写 equals 方法的时候,也要重写 hashCode 方法,这样才能保持条件判断的同步

个人建议不管会不会用到散列表,只要你重写 equals 就一起重写 hashCode ,这样肯定不会出错。


HashCode 的作用

Java 的集合有两类,一类是 List,还有一类是 Set。List是有序可重复,Set 无须不重复。

当我们在 set 集合中插入的时候怎么判断是否已经存在该元素呢,可通过 equals 方法。但是如果元素太多,用这样的方法就会比较慢。

于是就有人发明了 哈希算法来提高集合中查找元素的效率。

这种方式将集合分成若干个存储区域,每个存储的对象可以计算出一个哈希码,可以将哈希码范围分组划分,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的哪个区域。

hashCode 方法可以这样理解:

它返回的就是根据对象的内存地址换算出来的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的 equals 方法与新元素进行比较:相同的话就不存了,不相同就散列其他地址。这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次。

两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

不对,两个对象的 hashCode() 相同,equals() 不一定 true。

示例:

String str1 = "通话";String str2 = "重地";
System. out. println(String. format("str1:%d | str2:%d",  str1. hashCode(),str2. hashCode()));
System. out. println(str1. equals(str2));

执行的结果:

str1:1179395 | str2:1179395	
false

很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,

因为在散列表中,hashCode() 相等两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等,还有 equals 方法判断。只有 hashCode() 和 equals() 方法两个判断都相等才判断两个对象相等。

JAVA-技能点
知识点