1. 程式人生 > 其它 >Hash簡介以及Java HashCode的用途

Hash簡介以及Java HashCode的用途

Hash簡介以及Java HashCode的用途

Hash俗稱雜湊,在不同的語言中有不同的別名,學過資料結構的同學對此應該不陌生,最簡單的hash演算法取模如下

public int hashAlg(int origin){
       return origin % 10;
}

將輸入的引數對一個特定的數取模,得到一個特徵值,得到的那個值即為通常意義上的雜湊值(hashCode),
相較於傳統的通過key查詢資料,散列表/hash表查詢資料的方式通常需要將key進行一定的運算,得到hash值,然後用hash值進行定位查詢資料,
通常意義上的雜湊演算法的時間複雜度為O(1),這也是為什麼雜湊如此常見與流行的一個原因

眾所周知,Java的Object類是所有的型別的基類,Object中的方法列表如下:

 public Object() {}

 public final native Class<?> getClass();

 public native int hashCode();

 public boolean equals(Object obj) {
        return (this == obj);
 }

 protected native Object clone() throws CloneNotSupportedException;

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

 public final native void notify();

 public final native void notifyAll();

 public final void wait() throws InterruptedException {
        wait(0L);
 }

 public final native void wait(long timeoutMillis) throws InterruptedException;

 public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
       if (timeoutMillis < 0) {
              throw new IllegalArgumentException("timeoutMillis value is negative");
       }

       if (nanos < 0 || nanos > 999999) {
              throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
       }

       if (nanos > 0) {
              timeoutMillis++;
       }

       wait(timeoutMillis);
}

protected void finalize() throws Throwable { }

可以看到,裡面的大部分方法均為native方法,非native的方法除去equals方法,均為直接或者間接的呼叫了native的方法,預設的toString方法的實現也用到了hashCode方法,
本章的主角咋們來說說equalshashCode這兩個方法

一個老生常談的問題

大家在初學Java這門程式語言的時候,經常被問到的一個問題就是equals==在比較物件時候有什麼異同,String物件的equals==之間的區別,
進一步的知識點可能會被問到Byte這類冷門點的話題,諸如以下程式碼

       String abc = "abc";
       String abcObj = new String("abc");
       System.out.println(abc == "abc");
       System.out.println(abc == abcObj);

眾所周知,Java並不支援操作符過載,關於==比較的永遠是兩個物件的地址,想要實現C++中的操作符過載的效果,需要自己編碼(編寫方法)實現對應的功能,equals方法為Object的方法,
預設的equals方法比較的是兩個物件的記憶體地址,equals方法可以被子類重寫,對應的hashCode也可以被重寫,事實上這倆要重寫的話通常是需要成對進行重寫

那麼,hashCode與記憶體地址有什麼關係,事實上在現今JDK版本(JDK8往上)這倆並沒有實質性的關係,可以認為是兩個完全不同的東西,

  • 記憶體地址在程式的程序地址空間中是唯一的,記憶體由系統按需分配,故記憶體地址的值通常是不可預知的

  • 預設的hashCode實現由JVM底層的Native方法實現,感興趣的同學可以參見OpenJdk中關於hashCode實現的部分,對應的程式碼倉庫如下
    https://github.com/openjdk/jdk

對應的實現程式碼如下(可能需要切換分支,下述程式碼在jdk11的系列分支是存在的,檔名為synchronizer.cpp):

static inline intptr_t get_next_hash(Thread * Self, 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 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;
}

可以看到,native的實現總共有6種,從上往下的型別編號分別為0-5,依次排序,

  • 0:呼叫os::random() 生成hashCode,亦即使用一個隨機值做hashCode
  • 1:記憶體地址做移位操作,然後與steRandom(隨機數)做異或操作
  • 2:固定值1
  • 3:自增序列
  • 4:使用物件的記憶體地址
  • 5:當前執行緒中的四個數字(實際上是一個隨機數+三個確定值)運用xorshift隨機數演算法得到的一個隨機數

預設的實現為5,亦即上述列表中的最後一個,從註釋中也可以看到此演算法作者的本意在將來的版本中會成為預設的hashCode實現,實際上由於這部分程式碼提交歷史比較久了,此實現現在已經成為了預設的實現,對於上述規則中1和4,hashCode的計算確實與記憶體地址有一定的關係,早期版本的JDK實現是否採用此實現暫未考究,也不在系列討論的範疇中

thread.cpp中關於執行緒threadState x y z w的四個值的初始化如下:


  // thread-specific hashCode stream generator state - Marsaglia shift-xor form
  _hashStateX = os::random();
  _hashStateY = 842502087;
  _hashStateZ = 0x8767;    // (int)(3579807591LL & 0xffff) ;
  _hashStateW = 273326509;

可以看到上述值除開x外,y z w三個值均為固定值(通常又稱之為MagicNumber),下述程式碼為測試程式碼,可以測試預設的hashCode實現

 public static void main(String[] args) throws Exception {
        Object mainHashObj = new Object();
        String info = String.format("toString:%s,hex:%s,hashCode:%s", mainHashObj, Integer.toHexString(mainHashObj.hashCode()), mainHashObj.hashCode());
        System.out.println(info);
 }

注意,編譯執行上述程式碼需要加上jvm引數 -XX:+UnlockExperimentalVMOptions -XX:hashCode=2 引數,不然會使用jdk預設的hashCode演算法實現,下面是這倆引數的解釋:

  • UnlockExperimentalVMOptions 解鎖專家模式
  • hashCode=2 配置那種hash演算法為預設的,此處的2即為上述列表中的0-5,具體含義參見上述解釋,不在贅述

可以手動修改hashCode的值,重複執行上述程式碼,觀察預設的hashCode值的變化情況從而驗證