對Java中HashCode方法的深刻思考

前言

最近在學習 Go 語言,Go 語言中有指針對象,一個指針變量指向了一個值的內存地址。學習過 C 語言的猿友應該都知道指針的概念。Go 語言語法與 C 相近,能夠說是類 C 的編程語言,因此 Go 語言中有指針也是很正常的。咱們能夠經過將取地址符&放在一個變量前使用就會獲得相應變量的內存地址。java

package main

import "fmt"

func main() {
   var a int= 20   /* 聲明實際變量 */
   var ip *int        /* 聲明指針變量 */

   ip = &a  /* 指針變量的存儲地址 */
   fmt.Printf("a 變量的地址是: %x\n", &a  )

   /* 指針變量的存儲地址 */
   fmt.Printf("ip 變量儲存的指針地址: %x\n", ip )
   /* 使用指針訪問值 */
   fmt.Printf("*ip 變量的值: %d\n", *ip )
}
複製代碼

由於本人主要開發語言是 Java,因此我就聯想到 Java 中沒有指針,那麼 Java 中如何獲取變量的內存地址呢?編程

若是能獲取變量的內存地址那麼就能夠清晰的知道兩個對象是不是同一個對象,若是兩個對象的內存地址相等那麼無疑是同一個對象反之則是不一樣的對象。數組

不少人說對象的 HashCode 方法返回的就是對象的內存地址,包括我在《Java核心編程·卷I》的第5章內容中也發現說是 HashCode 其值就是對象的內存地址。app

可是 HashCode 方法真的是內存地址嗎? 回答這個問題前咱們先回顧下一些基礎知識。dom

==和equals

在 Java 中比較兩個對象是否相等主要是經過 ==號,比較的是他們在內存中的存放地址。Object 類是 Java 中的超類,是全部類默認繼承的,若是一個類沒有重寫 Object 的 equals方法,那麼經過equals方法也能夠判斷兩個對象是否相同,由於它內部就是經過==來實現的。jvm

//Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj) {
    return (this == obj);
}
複製代碼

Tips: 這裏額外解釋個疑惑編程語言

咱們學習 Java 的時候知道,Java 的繼承是單繼承,若是全部的類都繼承了 Object 類,那麼爲什麼建立一個類的時候還能夠extend其餘的類?分佈式

這裏涉及到直接繼承和間接繼承的問題,當建立的類沒有經過關鍵字 extend 顯示繼承指定的類時,類默認的直接繼承了Object,A --> Object。當建立的類經過關鍵字 extend 顯示繼承指定的類時,則它間接的繼承了Object類,A --> B --> Object。ide

這裏的相同,是說比較的兩個對象是不是同一個對象,即在內存中的地址是否相等。而咱們有時候須要比較兩個對象的內容是否相同,即類具備本身特有的「邏輯相等」概念,而不是想了解它們是否指向同一個對象。微服務

例如比較以下兩個字符串是否相同String a = "Hello"String b = new String("Hello"),這裏的相同有兩種情形,是要比較 a 和 b 是不是同一個對象(內存地址是否相同),仍是比較它們的內容是否相等?這個具體須要怎麼區分呢?

若是使用 == 那麼就是比較它們在內存中是不是同一個對象,可是 String 對象的默認父類也是 Object,因此默認的equals方法比較的也是內存地址,因此咱們要重寫 equals方法,正如 String 源碼中所寫的那樣。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
複製代碼

這樣當咱們 a == b時是判斷 a 和 b 是不是同一個對象,a.equals(b)則是比較 a 和 b 的內容是否相同,這應該很好理解。

JDK 中不止 String 類重寫了equals 方法,還有數據類型 Integer,Long,Double,Float等基本也都重寫了 equals 方法。因此咱們在代碼中用 Long 或者 Integer 作業務參數的時候,若是要比較它們是否相等,記得須要使用 equals 方法,而不要使用 ==

由於使用 ==號會有意想不到的坑出現,像這種數據類型不少都會在內部封裝一個常量池,例如 IntegerCache,LongCache 等等。當數據值在某個範圍內時會直接從常量池中獲取而不會去新建對象。

若是要使用==,能夠將這些數據包裝類型轉換爲基本類型以後,再經過==來比較,由於基本類型經過==比較的是數值,可是在轉換的過程當中須要注意 NPE(NullPointException)的發生。

Object中的HashCode

equals 方法能比較兩個對象的內容是否相等,所以能夠用來查找某個對象是否在集合容器中,一般大體就是逐一去取集合中的每一個對象元素與須要查詢的對象進行equals比較,當發現某個元素與要查找的對象進行equals方法比較的結果相等時,則中止繼續查找並返回確定的信息,不然,返回否認的信息。

可是經過這種比較的方式效率很低,時間複雜度比較高。那麼咱們是否能夠經過某種編碼方式,將每個對象都具備某個特定的碼值,根據碼值將對象分組而後劃分到不一樣的區域,這樣當咱們須要在集合中查詢某個對象時,咱們先根據該對象的碼值就能肯定該對象存儲在哪個區域,而後再到該區域中經過equals方式比較內容是否相等,就能知道該對象是否存在集合中。

經過這種方式咱們減小了查詢比較的次數,優化了查詢的效率同時也就減小了查詢的時間。

這種編碼方式在 Java 中就是 hashCode 方法,Object 類中默認定義了該方法, 它是一個 native 修飾的本地方法,返回值是一個 int 類型。

/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * ... * As much as is reasonably practical, the hashCode method defined by * class {@code Object} does return distinct integers for distinct * objects. (This is typically implemented by converting the internal * address of the object into an integer, but this implementation * technique is not required by the * Java™ programming language.) * * @return a hash code value for this object. * @see java.lang.Object#equals(java.lang.Object) * @see java.lang.System#identityHashCode */
public native int hashCode();
複製代碼

從註釋的描述能夠知道,hashCode 方法返回該對象的哈希碼值。它能夠爲像 HashMap 這樣的哈希表有益。Object 類中定義的 hashCode 方法爲不一樣的對象返回不一樣的整形值。具備迷惑異議的地方就是This is typically implemented by converting the internal address of the object into an integer這一句,意爲一般狀況下實現的方式是將對象的內部地址轉換爲整形值。

若是你不深究就會認爲它返回的就是對象的內存地址,咱們能夠繼續看看它的實現,可是由於這裏是 native 方法因此咱們沒辦法直接在這裏看到內部是如何實現的。native 方法自己非 java 實現,若是想要看源碼,只有下載完整的 jdk 源碼,Oracle 的 JDK 是看不到的,OpenJDK 或其餘開源 JRE 是能夠找到對應的 C/C++ 代碼。咱們在 OpenJDK 中找到 Object.c 文件,能夠看到hashCode 方法指向 JVM_IHashCode 方法來處理。

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};
複製代碼

JVM_IHashCode方法實如今 jvm.cpp中的定義爲:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))  
  JVMWrapper("JVM_IHashCode");  
  // as implemented in the classic virtual machine; return 0 if object is NULL 
  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;  
JVM_END 
複製代碼

這裏是一個三目表達式,真正計算得到 hashCode 值的是ObjectSynchronizer::FastHashCode,它具體的實如今synchronizer.cpp中,截取部分關鍵代碼片斷。

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  if (UseBiasedLocking) {
  
  ......
  
  // Inflate the monitor to set hash code
  monitor = ObjectSynchronizer::inflate(Self, obj);
  // Load displaced header and check it has hash code
  mark = monitor->header();
  assert (mark->is_neutral(), "invariant") ;
  hash = mark->hash();
  if (hash == 0) {
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash); // merge hash code into header
    assert (temp->is_neutral(), "invariant") ;
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
      // The only update to the header in the monitor (outside GC)
      // is install the hash code. If someone add new usage of
      // displaced header, please update this code
      hash = test->hash();
      assert (test->is_neutral(), "invariant") ;
      assert (hash != 0, "Trivial unexpected object/monitor header usage.");
    }
  }
  // We finally get the hash
  return hash;
}
複製代碼

從以上代碼片斷中能夠發現,實際計算hashCode的是 get_next_hash,還在這份文件中咱們搜索get_next_hash,獲得他的關鍵代碼。

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     // This form uses an unguarded global Park-Miller RNG,
     // so it's possible for two threads to race and generate the same 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 addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) {
     value = 1 ;            // for sensitivity testing
  } else
  if (hashCode == 3) {
     value = ++GVars.hcSequence ;
  } 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 = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}
複製代碼

get_next_hash的方法中咱們能夠看到,若是從0開始算的話,這裏提供了6種計算 hash 值的方案,有自增序列,隨機數,關聯內存地址等多種方式,其中官方默認的是最後一種,即隨機數生成。能夠看出 hashCode 也許和內存地址有關係,但不是直接表明內存地址的,具體須要看虛擬機版本和設置。

equals和hashCode

equals 和 hashCode 都是 Object 類擁有的方法,包括 Object 類中的 toString 方法打印的內容也包含 hashCode 的無符號十六進制值。

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
複製代碼

因爲須要比較對象內容,因此咱們一般會重寫 equals 方法,可是重寫 equals 方法的同時也須要重寫 hashCode 方法,有沒有想過爲何?

由於若是不這樣作的話,就會違反 hashCode 的通用約定,從而致使該類沒法結合全部基於散列的集合一塊兒正常工做,這類集合包括 HashMap 和 HashSet。

這裏的通用約定,從 Object 類的 hashCode 方法的註釋能夠了解,主要包括如下幾個方面,

  • 在應用程序的執行期間,只要對象的 equals 方法的比較操做所用到的信息沒有被修改,那麼對同一個對象的屢次調用,hashCode 方法都必須始終返回同一個值。

  • 若是兩個對象根據 equals 方法比較是相等的,那麼調用這兩個對象中的 hashCode 方法都必須產生一樣的整數結果。

  • 若是兩個對象根據 equals 方法比較是不相等的,那麼調用者兩個對象中的 hashCode 方法,則不必定要求 hashCode 方法必須產生不一樣的結果。可是給不相等的對象產生不一樣的整數散列值,是有可能提升散列表(hash table)的性能。

從理論上來講若是重寫了 equals 方法而沒有重寫 hashCode 方法則違背了上述約定的第二條,相等的對象必須擁有相等的散列值

可是規則是你們默契的約定,若是咱們就喜歡不走尋常路,在重寫了 equals 方法後沒有覆蓋 hashCode 方法,會產生什麼後果嗎?

咱們自定義一個 Student 類,而且重寫了 equals 方法,可是咱們沒有重寫 hashCode 方法,那麼當調用 Student 類的 hashCode 方法的時候,默認就是調用超類 Object 的 hashCode 方法,根據隨機數返回的一個整型值。

public class Student {

    private String name;

    private String gender;

    public Student(String name, String gender) {
        this.name = name;
        this.gender = gender;
    }

    //省略 Setter,Gettter
    
    @Override
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof Student) {
            Student anotherStudent = (Student) anObject;

            if (this.getName() == anotherStudent.getName()
                    || this.getGender() == anotherStudent.getGender())
                return true;
        }
        return false;
    }
}
複製代碼

咱們建立兩個對象而且設置屬性值同樣,測試下結果:

public static void main(String[] args) {

    Student student1 = new Student("小明", "male");
    Student student2 = new Student("小明", "male");

    System.out.println("equals結果:" + student1.equals(student2));
    System.out.println("對象1的散列值:" + student1.hashCode() + ",對象2的散列值:" + student2.hashCode());
}
複製代碼

獲得的結果

equals結果:true
對象1的散列值:1058025095,對象2的散列值:665576141
複製代碼

咱們重寫了 equals 方法,根據姓名和性別的屬性來判斷對象的內容是否相等,可是 hashCode 因爲是調用 Object 類的 hashCode 方法,因此打印的是兩個不相等的整型值。

若是這個對象咱們用 HashMap 存儲,將對象做爲 key,熟知 HashMap 原理的同窗應該知道,HashMap 是由數組 + 鏈表的結構組成,這樣的結果就是由於它們 hashCode 不相等,因此放在了數組的不一樣下標,當咱們根據 Key 去查詢的時候結果就爲 null。

public static void main(String[] args) {

    Student student1 = new Student("小明", "male");
    Student student2 = new Student("小明", "male");
    
    HashMap<Student, String> hashMap = new HashMap<>();
    hashMap.put(student1, "小明");

    String value = hashMap.get(student2);
    System.out.println(value); 
}
複製代碼

輸出結果

null
複製代碼

獲得的結果咱們確定不滿意,這裏的 student1 和 student2 雖然內存地址不一樣,可是它們的邏輯內容相同,咱們認爲它們應該是相同的。

這裏若是很差理解,猿友能夠將 Student 類換成 String 類思考下,String 類是咱們經常做爲 HashMap 的 Key 值使用的,試想若是 String 類只重寫了 equals 方法而沒有重寫 HashCode 方法,這裏將某個字符串 new String("s") 做爲 Key 而後 put 一個值,可是再根據 new String("s") 去 Get 的時候卻獲得 null 的結果,這是難以讓人接受的。

因此不管是理論的約定上仍是實際編程中,咱們重寫 equals 方法的同時總要重寫 hashCode 方法,請記住這點

雖然 hashCode 方法被重寫了,可是若是咱們想要獲取原始的 Object 類中的哈希碼,咱們能夠經過 System.identityHashCode(Object a)來獲取,該方法返回默認的 Object 的 hashCode 方法值,即便對象的 hashCode 方法被重寫了也不影響。

public static native int identityHashCode(Object x);
複製代碼

總結

若是 HashCode 不是內存地址,那麼 Java 中怎麼獲取內存地址呢?找了一圈發現沒有直接可用的方法。

後來想一想也許這是 Java 語言編寫者認爲沒有直接獲取內存地址的必要吧,由於 Java 是一門高級語言相對於機器語言的彙編或者 C 語言來講更抽象並隱藏了複雜性,由於畢竟是在 C 和 C++ 的基礎上進一步封裝的。並且因爲自動垃圾回收機制和對象年齡代的問題,Java 中對象的地址是會變化的,所以獲取實際內存地址的意義不大。

固然以上是博主本人本身的觀點,若是猿友有其餘不一樣的意見或看法也能夠留言,你們一塊兒共同探討。


我的公衆號:小菜亦牛

歡迎長按下圖關注公衆號:小菜亦牛!

按期爲你奉上分佈式,微服務等一線互聯網公司相關技術的講解和分析。

相關文章
相關標籤/搜索