equals() and hashcode() in Java.

118 阅读3分钟

1,equals()方法和hashcode()这两个方法是用来干什么的?

在Java中的任意类都是继承自Object,equals方法和hashcod方法这两个方法便是继承自Object,因此任意类都需要继承、实现equals方法,并且实现hashcode方法。任意类对象都持有这两个对象成员方法。

(1) equals方法其实很容易理解,它是是判断两个对象是否相等!

  • 在object中的默认实现(直接使用==比较两个对象):

     * @param   obj   the reference object with which to compare.
     * @return  {@code true} if this object is the same as the obj
     *          argument; {@code false} otherwise.
     * @see     #hashCode()
     * @see     java.util.HashMap
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }
    
  • 在String等包装类中的重写后的实现:

     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    
  • 在我们自定义的类中也可以自定义对象相等的实现,通过重写equals方法。比如自定义该类型对象恒等。

    public class Same {

    @Override
    public boolean equals(Object obj) {
        return true;
    }
    

    }

注:==和equals方法的不同。

判断对象是否相等的实现维度不同。==是通过语言的编译器层面去实现,在最终的运行时会解释为比较两个对象的地址。而equals方法则是是jdk代码层面的实现,对于程序员来说更加灵活。

(2) hashcode方法是用来计算对象的hashcode这一特殊值,它存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(用HashCode来代表对象就是在hash表中的位置).

  • hashcode方法在object类中是没有默认实现的。

     * @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();
    
  • hashcode方法在String类中的实现

     private int hash; // Default to 0
     /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
    
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    

可以看出每个对象都可以通过hashcode方法算出一个hashcode值,这个值一定程度上对不同对象做了区分,但是它不能完全区分两个对象,不同的对象的hashcode值有可能相等。

同时我们可以得出两个结论:如果两个对象相同,那么它们的hashCode值一定要相同;如果两个对象的hashCode相同,它们并不一定相同。

貌似这个hashcode远不如equals方法准确,那为啥需要它?在一个集合中去查找一个对象的场景下,它能加速查找是hashcode方法存在的一个重要原因。如果我们只用equals方法势必需要挨个去比较。而我们通过hashcode可以在HashMap、HashSet等利用hashcode来加速查找,然后再使用equals方法来查到最终的目标对象。

2,为什么重写了equals()方法必须重写hashcode()方法?

1),如果我们理解了hashcode的存在意义,就很好理解为什么。一般来说,我们重写equals方法的目标是自定义对象相等的条件,这个时候如果我们不重写hashcode方法,就有可能导致equals方法判定为相等的对象的hashcode值不同,那么在使用hashcode加速查找的场景,如在HashMap、HashSet等集合中使用hashcode进行加速查找时就会发现找不到对象的情况,就发生了错误,因此我们约定在重写equals方法时也重写hashcode方法。

2)因为我们需要满足,如果两个对象相同,那么它们的hashCode值一定要相同;这样hashcode才能发挥它的作用。

我们可以看看HashMap中是如何使用hashcode和equals方法的。这里以put方法的实现为例。

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

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

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;
            }
        }

其实无论是put方法还是get方法,都是先利用hashcode值的散列值找到一个hash表的槽位后再使用equals方法来找到最终的目标。如果我们重写了equals没有重写hashcode,相同的对象的hashcode不同,那很容易就出现查不到对应槽位的情况。