2017年10月8日 星期日

實作 hashCode() 與使用 Set 的陷井

如果你有實作過 hashCode(),或是有仔細看過 Object.hashCode() 的 API 說明,那麼你應該會知道它有以下的規則:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
但沒想到,這裡卻藏著陷井

以下先試著翻譯:

  • 在 Java 應用程式的一個執行動作的期間,無論在同一個物件上呼叫幾次 hashCode(),它都必須始終回傳相同的整數,前提是在物件上的equals() 比較中所會使用到的任何資訊都沒有被修改。但該整數不需要與在相同應用程式中的另一個執行動作中所呼叫取得的整數保持一致。(希望這段我沒理解錯意思)
  • 如果兩物件以 equals() 比較後是相等的,那麼呼叫這兩個物件的 hashCode() 所得的整數必須要一樣
  • 當兩物件以 equals() 比較後是不相等的,它們 hashCode() 所回傳的整數並不一定要不一樣(也就是可以是一樣的)
再來就來看有什麼陷井吧
若到網路上搜尋一下如何實作 hashCode(),可能會常看到類似這樣的例子:

public class Employee {
 private Integer id;
 //其他屬性(略)
 public Integer getId() {
  return this.id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 //其他 getter setter (略)
 //public boolean equals(Object o) {
 // 略,基本上是以 id 做比較
 //}
 @Override
 public int hashCode() {
  final int PRIME = 31;
  int result = 1;
  Integer id = this.getId();
  result = PRIME * result + (id==null?0:id);
  return result;
 }
}

也就是 hashCode() 是用 id 為基礎來產生的
如果資料都是從資料庫讀出的,那麼通常 id 是不變的,所以基本上沒什麼問題,但如果這是一筆新資料,還未產生 id (即 null),此時 hashCode 為 31,這時候將它放入 HashSet 裡,然後改變 id 之後,再用 set.contains() 來確認是否存在於 set 的話,那就會得到 false,因為 id 改變後,hashCode 也跟著改變了,而 HashSet 是基於 hashCode 做索引的,因此改變 hashCode 後當然就查不到啦

以下是實測的範例:

package test;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class TestHashCode {
 public static void main(String[] args) {
  testEmployee();
 }
 public static void testEmployee(){
  Set set = new HashSet();
  Employee e = new Employee();
  set.add(e);
  System.out.println("1. ->"+ set.contains(e));
  e.setId(10);
  System.out.println("2. ->"+ set.contains(e));
 }
 public static class Employee {
  private Integer id;
  //其他屬性(略)
  public Integer getId() {
   return this.id;
  }
  public void setId(Integer id) {
   this.id = id;
  }
  //其他 getter setter (略)
  //public boolean equals(Object o) {
  // 略,基本上是以 id 做比較
  //}
  @Override
  public int hashCode() {
   final int PRIME = 31;
   int result = 1;
   Integer id = this.getId();
   result = PRIME * result + (id==null?0:id);
   return result;
  }
 }
}

執行後得到的結果為
1. ->true
2. ->false

除了自己實作的 JavaBean 有這個問題之外,Map 也有這個問題,以下是實測的例子:

package test;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class TestHashCode {
 public static void main(String[] args) {
  testMap();
 }
 private static void testMap(){
  Set set = new HashSet();
  Map map = new HashMap();
  set.add(map);
  System.out.println("1. hashCode="+map.hashCode());
  System.out.println("1. ->"+set.contains(map));
  map.put("a", "hi a");
  System.out.println("2. hashCode="+map.hashCode());
  System.out.println("2. ->"+set.contains(map));
 }
}

執行結果為:
1. hashCode=0
1. ->true
2. hashCode=3200355
2. ->false

我也不能說這樣實作 hashCode() 是不對的,因為它確實是符合規則的,所以自己要非常小心,要把這樣的狀況記在心裡,然後在應用時,避開會造成問題的使用方式,免得會得到意想不到的結果

以上經驗分享給大家

沒有留言:

張貼留言

廣告訊息會被我刪除