天天看點

深入 Java 源碼來剖析 hashCode,從此菜不再是原罪(2)

當然了,從理論上來說,對于兩個不同對象,它們通過 hashCode() 方法計算後的值可能相同。是以,不能使用 hashCode() 方法來判斷兩個對象是否相等,必須得通過 equals() 方法。

也就是說:

如果兩個對象調用 equals() 方法得到的結果為 true,調用 hashCode() 方法得到的結果必定相等;

如果兩個對象調用 hashCode() 方法得到的結果不相等,調用 equals() 方法得到的結果必定為 false;

反之:

如果兩個對象調用 equals() 方法得到的結果為 false,調用 hashCode() 方法得到的結果不一定不相等;

如果兩個對象調用 hashCode() 方法得到的結果相等,調用 equals() 方法得到的結果不一定為 true;

來看下面這段代碼。

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student(18, "張三");
        Map<Student, Integer> scores = new HashMap<>();
        scores.put(s1, 98);
        System.out.println(scores.get(new Student(18, "張三")));
    }
}
 class Student {
    private int age;
    private String name;
     public Student(int age, String name) {
         this.age = age;
         this.name = name;
     }
     @Override
     public boolean equals(Object o) {
         Student student = (Student) o;
         return age == student.age &&
                 Objects.equals(name, student.name);
     }
 }      

我們重寫了 Student 類的 equals() 方法,如果兩個學生的年紀和姓名相同,我們就認為是同一個學生,雖然很離譜,但我們就是這麼草率。

在 main() 方法中,18 歲的張三考試得了 98 分,很不錯的成績,我們把張三和成績放到了 HashMap 中,然後準備輸出張三的成績:

null

很不巧,結果為 null,而不是預期當中的 98。這是為什麼呢?

原因就在于重寫 equals() 方法的時候沒有重寫 hashCode() 方法。預設情況下,hashCode() 方法是一個本地方法,會傳回對象的存儲位址,顯然 put() 中的 s1 和 get() 中的 new Student(18, "張三") 是兩個對象,它們的存儲位址肯定是不同的。

HashMap 的 get() 方法會調用 hash(key.hashCode()) 計算對象的哈希值,雖然兩個不同的 hashCode() 結果經過 hash() 方法計算後有可能得到相同的結果,但這種機率微乎其微,是以就導緻 scores.get(new Student(18, "張三")) 無法得到預期的值 18。

怎麼解決這個問題呢?很簡單,重寫 hashCode() 方法。

@Override
 public int hashCode() {
     return Objects.hash(age, name);
 }
Objects 類的 hash() 方法可以針對不同數量的參數生成新的 hashCode() 值。
public static int hashCode(Object a[]) {
 if (a == null)
     return 0;
 int result = 1;
 for (Object element : a)
     result = 31 * result + (element == null ? 0 : element.hashCode());
 return result;
}      

代碼似乎很簡單,歸納出的數學公式如下所示(n 為字元串長度)。

注意:31 是個奇質數,不大不小,一般質數都非常适合哈希計算,偶數相當于移位運算,容易溢出,造成資料資訊丢失。

這就意味着年紀和姓名相同的情況下,會得到相同的哈希值。scores.get(new Student(18, "張三")) 就會傳回 98 的預期值了。

《Java 程式設計思想》這本聖經中有一段話,對 hashCode() 方法進行了一段描述。

設計 hashCode() 時最重要的因素就是:無論何時,對同一個對象調用 hashCode() 都應該生成同樣的值。如果在将一個對象用 put() 方法添加進 HashMap 時産生一個 hashCode() 值,而用 get() 方法取出時卻産生了另外一個 hashCode() 值,那麼就無法重新取得該對象了。是以,如果你的 hashCode() 方法依賴于對象中易變的資料,使用者就要當心了,因為此資料發生變化時,hashCode() 就會生成一個不同的哈希值,相當于産生了一個不同的鍵。

也就是說,如果在重寫 hashCode() 和 equals() 方法時,對象中某個字段容易發生改變,那麼最好舍棄這些字段,以免産生不可預期的結果。

好。有了上面這些内容作為基礎後,我們回頭再來看看本地方法 hashCode() 的 C++ 源碼。

static inline intptr_t get_next_hash(Thread* current, oop obj) {
  intptr_t value = 0;
  if (hashCode == 0) {
    // This form uses global Park-Miller RNG.
    // On MP system we'll have lots of RW access to a global, so the
    // mechanism induces lots of coherency traffic.
    value = os::random();
  } else if (hashCode == 1) {
    // This variation has the property of being stable (idempotent)
    // between STW operations.  This can be useful in some of the 1-0
    // synchronization schemes.
    intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
    value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
  } else if (hashCode == 2) {
    value = 1;            // for sensitivity testing
  } else if (hashCode == 3) {
    value = ++GVars.hc_sequence;
  } else if (hashCode == 4) {
    value = cast_from_oop<intptr_t>(obj);
  } else {
    // Marsaglia's xor-shift scheme with thread-specific state
    // This is probably the best overall implementation -- we'll
    // likely make this the default in future releases.
    unsigned t = current->_hashStateX;
    t ^= (t << 11);
    current->_hashStateX = current->_hashStateY;
    current->_hashStateY = current->_hashStateZ;
    current->_hashStateZ = current->_hashStateW;
    unsigned v = current->_hashStateW;
    v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
    current->_hashStateW = v;
    value = v;
  }
  value &= markWord::hash_mask;
  if (value == 0) value = 0xBAD;
  assert(value != markWord::no_hash, "invariant");
  return value;
}      

如果沒有 C++ 基礎的話,不用細緻去看每一行代碼,我們隻通過表面去了解一下 get_next_hash() 這個方法就行。其中的 hashCode 變量是 JVM 啟動時的一個全局參數,可以通過它來切換哈希值的生成政策。

hashCode==0,調用作業系統 OS 的 random() 方法傳回随機數。

hashCode == 1,在 STW(stop-the-world)操作中,這種政策通常用于同步方案中。利用對象位址進行計算,使用不經常更新的随機數(GVars.stw_random)參與其中。

hashCode == 2,使用傳回 1,用于某些情況下的測試。

hashCode == 3,從 0 開始計算哈希值,不是線程安全的,多個線程可能會得到相同的哈希值。

hashCode == 4,與建立對象的記憶體位置有關,原樣輸出。

hashCode == 5,預設值,支援多線程,使用了 Marsaglia 的 xor-shift 算法産生僞随機數。所謂的 xor-shift 算法,簡單來說,看起來就是一個移位寄存器,每次移入的位由寄存器中若幹位取異或生成。所謂的僞随機數,不是完全随機的,但是真随機生成比較困難,是以隻要能通過一定的随機數統計檢測,就可以當作真随機數來使用。

至于更深層次的挖掘,涉及到數學知識和實體知識,就不展開了。畢竟菜是原罪。

我最近花了近一周的時間整理了一份純 Java 版的刷題筆記,一共 300 道題解!

圖文并茂,截圖如下,不隻是幹巴巴的題解代碼,很多題都給出了多種解題思路,真的會提高大家刷題的幸福指數~

深入 Java 源碼來剖析 hashCode,從此菜不再是原罪(2)