1. 程式人生 > >HashCode()與equals()深入理解

HashCode()與equals()深入理解

1、hashCode()和equals()方法都是Object類提供的方法,

    hashCode()返回該物件的雜湊碼值,該值通常是一個由該物件的內部地址轉換而來的int型整數,

 Object的equals()方法等價於==,也就是判斷兩個引用的物件是否是同一物件,所謂同一物件就是指記憶體中同一塊儲存單元

2、要判斷兩個物件邏輯相等就要覆蓋equals()方法,當覆蓋equals()方法時建議覆蓋hashCode()方法,

官方hashCode的常規協定是如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。 

 

3、在一些雜湊儲存結構的集合中(Hashset,HashMap...)判斷兩個物件是否相等是先判斷兩個物件的hashCode是否相等,再判斷兩個物件用equals()運算是否相等

4、hashCode是為了提高在雜湊結構儲存中查詢的效率,線上性表中沒有作用。

5、若兩個物件equals返回true,則hashCode有必要也返回相同的int數。

6、同一物件在執行期間若已經儲存在集合中,則不能修改影響hashCode值的相關資訊,否則會導致記憶體洩露問題。

一、equals()方法

equals是Object類提供的方法之一,眾所周知,每一個java類都繼承自Object類,所以說每一個物件都有equals這個方法。而我們在用這個方法時卻一般都重寫這個方法,why?

Object類中equals()方法的原始碼:

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

從這個方法中可以看出,只有當一個例項等於它本身的時候,equals()才會返回true值。通俗地說,此時比較的是兩個引用是否指向記憶體中的同一個物件,也可以稱做是否例項相等。而我們在使用equals()來比較兩個指向值物件的引用的時候,往往希望知道它們邏輯上是否相等,而不是它們是否指向同一個物件——這就是我們通常重寫這個方法的原因。

重寫equals()方法,必須要遵守通用約定。來自java.lang.Object的規範,equals方法實現了等價關係,以下是要求遵循的5點:

1.自反性:對於任意的引用值x,x.equals(x)一定為true。 

2.對稱性:對於任意的引用值x 和 y,當x.equals(y)返回true時,y.equals(x)也一定返回true。 

3.傳遞性:對於任意的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)也一定返回true。

4. 一致性:對於任意的引用值x 和y,如果用於equals比較的物件資訊沒有被修改,多次呼叫x.equals(y)要麼一致地返回true,要麼一致地返回false。
5.非空性:對於任意的非空引用值x,x.equals(null)一定返回false。

二、hashCode()方法

hashcode()這個方法也是從object類中繼承過來的,在object類中定義如下:

public native int hashCode();

 hashCode()返回該物件的雜湊碼值,該值通常是一個由該物件的內部地址轉換而來的整數,它的實現主要是為了提高雜湊表(例如java.util.Hashtable提供的雜湊表)的效能。

官方文件給出的hashCode()的常規協定:

1、在 Java 應用程式執行期間,在同一物件上多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是物件上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。

2、如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。

3、以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼在兩個物件中的任一物件上呼叫 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的物件生成不同整數結果可以提高雜湊表的效能。

4、實際上,由 Object 類定義的 hashCode 方法確實會針對不同的物件返回不同的整數。(這一般是通過將該物件的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧。)

總結:

hashCode()的返回值和equals()的關係如下:

     如果x.equals(y)返回“true”,那麼x和y的hashCode()必須相等。 

     如果x.equals(y)返回“false”,那麼x和y的hashCode()有可能相等,也有可能不等。

重寫hashCode時注意事項

(1)返回的hash值是int型的,防止溢位。

(2)不同的物件返回的hash值應該儘量不同。(為了hashMap等集合的效率問題)

(3)《Java程式設計思想》中提到一種情況

“設計hashCode()時最重要的因素就是:無論何時,對同一個物件呼叫hashCode()都應該產生同樣的值。如果在講一個物件用put()新增進HashMap時產生一個hashCdoe值,而用get()取出時卻產生了另一個hashCode值,那麼就無法獲取該物件了。所以如果你的hashCode方法依賴於物件中易變的資料,使用者就要當心了,因為此資料發生變化時,hashCode()方法就會生成一個不同的雜湊碼”。

下面來看一張物件放入雜湊集合的流程圖:

 

 

 在儲存一個物件時,先進行hashCode值的比較,然後進行equals的比較。來認識一下具體hashCode和equals在程式碼中是如何呼叫的。

測試一:覆蓋equals(Object obj)但不覆蓋hashCode(),導致資料不唯一性

public class HashCodeTest {
    public static void main(String[] args) {
        Collection set = new HashSet();
        Point p1 = new Point(1, 1);
        Point p2 = new Point(1, 1);
 
        System.out.println(p1.equals(p2));
        set.add(p1); // (1)
        set.add(p2); // (2)
        set.add(p1); // (3)
 
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object object = iterator.next();
            System.out.println(object);
        }
    }
}
 
class Point {
    private int x;
    private int y;
 
    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Point other = (Point) obj;
        if (x != other.x) {
            return false;
        }
        if (y != other.y) {
            return false;
        }
        return true;
    }
 
    @Override
    public String toString() {
        return "x:" + x + ",y:" + y;
    }
 
}
//結果:
true
x:1,y:1
x:1,y:1

原因分析:

(1)當執行set.add(p1)時集合為空,直接存入集合;

(2)當執行set.add(p2)時首先判斷該物件(p2)的hashCode值所在的儲存區域是否有相同的hashCode,因為沒有覆蓋hashCode方法,所以jdk使用預設Object的hashCode方法,返回記憶體地址轉換後的整數,因為不同物件的地址值不同,所以這裡不存在與p2相同hashCode值的物件,因此jdk預設不同hashCode值,equals一定返回false,所以直接存入集合。

 (3)當執行set.add(p1)時,時,因為p1已經存入集合,同一物件返回的hashCode值是一樣的,繼續判斷equals是否返回true,因為是同一物件所以返回true。此時jdk認為該物件已經存在於集合中,所以捨棄。

測試二:覆蓋hashCode方法,但不覆蓋equals方法,仍然會導致資料的不唯一性

public class HashCodeTest {
    public static void main(String[] args) {
        Collection set = new HashSet();
        Point p1 = new Point(1, 1);
        Point p2 = new Point(1, 1);
 
        System.out.println(p1.equals(p2));
        set.add(p1); // (1)
        set.add(p2); // (2)
        set.add(p1); // (3)
 
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object object = iterator.next();
            System.out.println(object);
        }
    }
}
 
class Point {
    private int x;
    private int y;
 
    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }
 
    @Override
    public String toString() {
        return "x:" + x + ",y:" + y;
    }
 
}
//結果
false
x:1,y:1
x:1,y:1

原因分析:

(1)當執行set.add(p1)時(1),集合為空,直接存入集合;

(2)當執行set.add(p2)時(2),首先判斷該物件(p2)的hashCode值所在的儲存區域是否有相同的hashCode,這裡覆蓋了hashCode方法,p1和p2的hashCode相等,所以繼續判斷equals是否相等,因為這裡沒有覆蓋equals,預設使用'=='來判斷,所以這裡equals返回false,jdk認為是不同的物件,所以將p2存入集合。

 (3)當執行set.add(p1)時(3),時,因為p1已經存入集合,同一物件返回的hashCode值是一樣的,並且equals返回true。此時jdk認為該物件已經存在於集合中,所以捨棄。

綜合上述兩個測試,要想保證元素的唯一性,必須同時覆蓋hashCode和equals才行。

(注意:在HashSet中插入同一個元素(hashCode和equals均相等)時,會被捨棄,而在HashMap中插入同一個Key(Value 不同)時,原來的元素會被覆蓋。)

測試三:在記憶體洩露問題

public class HashCodeTest {
    public static void main(String[] args) {
        Collection set = new HashSet();
        Point p1 = new Point(1, 1);
        Point p2 = new Point(1, 2);
 
        set.add(p1);
        set.add(p2);
 
        p2.setX(10);
        p2.setY(10);
 
        set.remove(p2);
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object object = iterator.next();
            System.out.println(object);
        }
    }
}
 
class Point {
    private int x;
    private int y;
 
    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
 
    public int getX() {
        return x;
    }
 
    public void setX(int x) {
        this.x = x;
    }
 
    public int getY() {
        return y;
    }
 
    public void setY(int y) {
        this.y = y;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Point other = (Point) obj;
        if (x != other.x) {
            return false;
        }
        if (y != other.y) {
            return false;
        }
        return true;
    }
 
    @Override
    public String toString() {
        return "x:" + x + ",y:" + y;
    }
 
}
x:1,y:1
x:10,y:10

原因分析:

       假設p1的hashCode為1,p2的hashCode為2,在儲存時p1被分配在1號桶中,p2被分配在2號筒中。這時修改了p2中與計算hashCode有關的資訊(x和y),當呼叫remove(Object obj)時,首先會查詢該hashCode值得物件是否在集合中。假設修改後的hashCode值為10(仍存在2號桶中),這時查詢結果空,jdk認為該物件不在集合中,所以不會進行刪除操作。然而使用者以為該物件已經被刪除,導致該物件長時間不能被釋放,造成記憶體洩露。解決該問題的辦法是不要在執行期間修改與hashCode值有關的物件資訊,如果非要修改,則必須先從集合中刪除,更新資訊後再加入集合中。

測試4:

public class RectObject {  
    public int x;  
    public int y;  
    public RectObject(int x,int y){  
        this.x = x;  
        this.y = y;  
    }  
    @Override  
    public int hashCode(){  
        final int prime = 31;  
        int result = 1;  
        result = prime * result + x;  
        result = prime * result + y;  
        return result;  
    }  
    @Override  
    public boolean equals(Object obj){  
        return false;
    }  
}
public static void main(String[] args){  
        HashSet<RectObject> set = new HashSet<RectObject>();  
        RectObject r1 = new RectObject(3,3);  
        RectObject r2 = new RectObject(5,5);  
        RectObject r3 = new RectObject(3,3);  
        set.add(r1);  
        set.add(r2);  
        set.add(r3);  
        set.add(r1);  
        System.out.println("size:"+set.size());  
    }  

執行結果:size:3 

原因分析:

首先r1和r2的物件比較hashCode,不相等,所以r2放進set中,

再來看一下r3,比較r1和r3的hashCode方法,是相等的,然後比較他們兩的equals方法,因為equals方法始終返回false,所以r1和r3也是不相等的,r3和r2就不用說了,他們兩的hashCode是不相等的,所以r3放進set中,

再看r4,比較r1和r4發現hashCode是相等的,在比較equals方法,因為equals返回false,所以r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,所以r4可以放到set集合中,那麼結果應該是size:4,那為什麼會是3呢?

這時候我們就需要檢視HashSet的原始碼了,下面是HashSet中的add方法的原始碼:

    /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

這裡我們可以看到其實HashSet是基於HashMap實現的,hashset存放的元素作為hashMap裡面唯一的key變數,value部分用一個PRESENT物件來儲存。

我們在點選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;
        // 如果儲存元素的table為空,則進行必要欄位的初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;    // 獲取長度(16)
        // 如果根據hash值獲取的結點為空,則新建一個結點
     //(先查詢對應的索引位置有沒有元素)
if ((p = tab[i = (n - 1) & hash]) == null)      // 此處 & 代替了 % (除法雜湊法進行雜湊)             tab[i] = newNode(hash, key, value, null);         // 這裡的p結點是根據hash值算出來對應在陣列中的元素         else {             Node<K,V> e; K k;             // 如果新插入的結點和table中p結點的hash值,key值相同的話
        //這裡判斷hashCode是否相等,再判斷兩個物件是否相等或者兩個物件的equals方法,因為r1和r4是同一物件,
        //所以其實這裡是r4覆蓋了r1             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);                         // 連結串列長度大於8時,將連結串列轉紅黑樹                         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                     p = e;                 }             }             // 如果存在這個對映就覆蓋             if (e != null) { // existing mapping for key                 V oldValue = e.value;                 // 判斷是否允許覆蓋,並且value是否為空
                if (!onlyIfAbsent || oldValue == null)                     e.value = value;                 afterNodeAccess(e);     // 回撥以允許LinkedHashMap後置操作                 return oldValue;             }         }         ++modCount;     // 更改操作次數         if (++size > threshold)     // 大於臨界值             // 將陣列大小設定為原來的2倍,並將原先的陣列中的元素放到新陣列中             // 因為有連結串列,紅黑樹之類,因此還要調整他們             resize();           // 回撥以允許LinkedHashMap後置操作         afterNodeInsertion(evict);         return null;     }

 

參考:

https://blog.csdn.net/u012088516/article/details/86495512

https://blog.csdn.net/wonad12/article/details/78958411

https://blog.csdn.net/AJ1101/article/details/79413939

&n