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 ? e2==null : 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