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