hashCode方法與equals方法

工作中編寫代碼的時候涉及到了重寫equals方法和hashCode方法,一直都是重寫equals方法時要重寫hashCode方法,但是一直不知道原理,現在將學習到知識記錄下來。(文章引用的JDK源碼均爲1.8版本)

先來幾個問題:

1. hashCode和equals的作用都是什麼?

2. 爲什麼需要重寫equals()方法?

3. 爲什麼重寫equals方法時需要同時重寫HashCode方法?

 

hashCode和equals的作用都是什麼?

hashCode的作用:hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap,HashSet等,hashCode是用來在散列存儲結構中確定對象的存儲地址的。

equals的作用:equals是用於比較兩個對象的是否相等的。

在Object類中,equals方法的源碼如下:

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

通過以上代碼可以看出,在Object的equals方法中是比較兩個對象的地址是否相等,地址相等才認爲是相等。

Java中的很多類都重寫了這兩個方法,例如String類,Integer Long Double等包裝類,String類的equals方法源碼如下:

    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) {// while循環, 逐個字符比較是否相等
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

 

爲什麼需要重寫equals()方法?

我們在定義類時,我們經常會希望兩個不同對象的某些屬性值相同時就認爲他們相同,所以我們要重寫equals()方法。舉例如下

package com.jd.real.stock.web.http.impl;

import java.util.Objects;

public class People {

    /*身份證號*/
    String idCard;

    /*名字*/
    String name;

    public People() {}

    public People(String idCard, String name) {
        this.idCard = idCard;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        People people = (People) o;
        return Objects.equals(idCard, people.idCard);
    }

    @Override
    public int hashCode() {

        return Objects.hash(idCard);
    }

    public static void main(String[] args) {
        People oneInFamily = new People("21042319930101666x", "小明");
        People oneInCompany = new People("21042319930101666x", "黃小明");
        // 雖然在家裏和在公司他們的名字不一樣, 但是通過身份證Id可以識別他們就是同一個人
        System.out.println("Are they the same person?" + oneInCompany.equals(oneInFamily));
    }
}


----------------------------------------------
Are they the same person?true

Process finished with exit code 0

 

爲什麼重寫equals方法時需要同時重寫HashCode方法?

前面講到了,hashCode是用來在散列存儲結構中確定對象的存儲地址的。既然談到散列的存儲結構,挑兩個典型的HashSet和HashMap瞭解一下原理。

HashSet的add(Objcect o)方法源碼如下:


public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    public HashSet() {
        map = new HashMap<>();
    }

    public boolean add(E e) {
// HashSet的底層實現原理實際是使用的HashMap,利用HashMap結構的Key來實現集合的去重效果
        return map.put(e, PRESENT)==null;
    }

}

HashMap的put源碼如下:

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


    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);// 如果出現Hash衝突, 以鏈表的結構存儲新的Value,當前Hash地址下的老Value不會被刪除
                        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;
    }

通過HashSet的add源碼以及hashMap的put源碼可以看出來,HashSet底層是使用HahMap實現的。HashMap的Key當出現Hash衝突的時候會判斷當前Key和之前位置的Key是不是euqals,如果是同一個Key那麼新的value會直接覆蓋舊的Value。如果不equals,那麼會在同一個Hash桶中以鏈表的形式存儲下來。

掌握了以上知識,下面來說明爲什麼重寫equals需要同時重寫HashCode。

場景1:假如重寫了equals沒有重寫HashCode,那Object中原始的hashCode方法時比較兩個對象的內存地址是否相等。對於上述例子中,小明和黃小明顯然是同一個人,如果沒有重寫hashCode, 將小明和黃小明存儲到hashSet中時,就會發現都存入成功了(因爲兩個對象的HashCode不一致),就違反了HashSet的不可重複性。


場景2:假如重寫了HashCode,但是hashCode的返回值是固定值。則會出現equals不等,HashCode相同的情況。針對這種場景,當存入HashMap時,由於大家的HashCode相同,所以所有對象都會落入同一個哈希桶中,這樣哈希表就完全當做鏈表了,就失去了快速查找的特性了。

 

總結:


1、重寫equals時一定要重寫HashCode
2、如果equals不相等,HashCode一定也不相等。(否則就會出現場景2的問題)
3、如果equals相等,HashCode一定也相等。(否則就會出現場景1的問題)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章