為了徹底搞懂 hashCode故爵,我連 JDK 的源碼都沒放過

今天我們來談談 Java 中的 hashCode() 方法玻粪。眾所周知隅津,Java 是一門面向對象的編程語言,所有的類都會默認繼承自 Object 類劲室,而 Object 的中文意思就是“對象”伦仍。

Object 類中就包含了 hashCode() 方法:

@HotSpotIntrinsicCandidate
public native int hashCode();

意味著所有的類都會有一個 hashCode() 方法,該方法會返回一個 int 類型的值很洋。由于 hashCode() 方法是一個本地方法(native 關鍵字修飾的方法充蓝,用 C/C++ 語言實現(xiàn),由 Java 調用)喉磁,意味著 Object 類中并沒有給出具體的實現(xiàn)谓苟。

具體的實現(xiàn)可以參考 jdk/src/hotspot/share/runtime/synchronizer.cpp(源碼可以到 GitHub 上 OpenJDK 的倉庫中下載)。get_next_hash() 方法會根據(jù) hashCode 的取值來決定采用哪一種哈希值的生成策略协怒。

并且 hashCode() 方法被 @HotSpotIntrinsicCandidate 注解修飾涝焙,說明它在 HotSpot 虛擬機中有一套高效的實現(xiàn),基于 CPU 指令孕暇。

我在 簡書 共輸出了 100 多篇 Java 方面的文章仑撞,總字數(shù)超過 30 萬字, 內(nèi)容風趣幽默妖滔、通俗易懂隧哮,收獲了很多初學者的認可和支持,內(nèi)容包括 Java 語法座舍、Java 集合框架沮翔、Java 并發(fā)編程、Java 虛擬機等核心內(nèi)容曲秉。

image

為了幫助更多的 Java 初學者采蚀,我“一怒之下”就把這些文章重新整理并開源到了 GitHub,起名《教妹學 Java》岸浑,聽起來是不是就很有趣搏存?

GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java

那大家有沒有想過這樣一個問題:為什么 Object 類需要一個 hashCode() 方法呢瑰步?

在 Java 中矢洲,hashCode() 方法的主要作用就是為了配合哈希表使用的。

哈希表(Hash Table)缩焦,也叫散列表读虏,是一種可以通過關鍵碼值(key-value)直接訪問的數(shù)據(jù)結構,它最大的特點就是可以快速實現(xiàn)查找袁滥、插入和刪除盖桥。其中用到的算法叫做哈希,就是把任意長度的輸入题翻,變換成固定長度的輸出揩徊,該輸出就是哈希值。像 MD5、SHA1 都用的是哈希算法塑荒。

像 Java 中的 HashSet熄赡、Hashtable(注意是小寫的 t)、HashMap 都是基于哈希表的具體實現(xiàn)齿税。其中的 HashMap 就是最典型的代表彼硫,不僅面試官經(jīng)常問,工作中的使用頻率也非常的高凌箕。

大家想一下拧篮,如果沒有哈希表,但又需要這樣一個數(shù)據(jù)結構牵舱,它里面存放的數(shù)據(jù)是不允許重復的串绩,該怎么辦呢?

要不使用 equals() 方法進行逐個比較芜壁?這種方案當然是可行的赏参。但如果數(shù)據(jù)量特別特別大,采用 equals() 方法進行逐個對比的效率肯定很低很低沿盅,最好的解決方案就是哈希表把篓。

拿 HashMap 來說吧。當我們要在它里面添加對象時腰涧,先調用這個對象的 hashCode() 方法韧掩,得到對應的哈希值,然后將哈希值和對象一起放到 HashMap 中窖铡。當我們要再添加一個新的對象時:

  • 獲取對象的哈希值疗锐;
  • 和之前已經(jīng)存在的哈希值進行比較,如果不相等费彼,直接存進去滑臊;
  • 如果有相等的,再調用 equals() 方法進行對象之間的比較箍铲,如果相等雇卷,不存了;
  • 如果不等颠猴,說明哈希沖突了关划,增加一個鏈表,存放新的對象翘瓮;
  • 如果鏈表的長度大于 8贮折,轉為紅黑樹來處理。

就這么一套下來资盅,調用 equals() 方法的頻率就大大降低了调榄。也就是說踊赠,只要哈希算法足夠的高效,把發(fā)生哈希沖突的頻率降到最低每庆,哈希表的效率就特別的高臼疫。

來看一下 HashMap 的哈希算法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

先調用對象的 hashCode() 方法,然后對該值進行右移運算扣孟,然后再進行異或運算烫堤。

通常來說,String 會用來作為 HashMap 的鍵進行哈希運算凤价,因此我們再來看一下 String 的 hashCode() 方法:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                : StringUTF16.hashCode(value);
    }
    return h;
}
public static int hashCode(byte[] value) {
    int h = 0;
    int length = value.length >> 1;
    for (int i = 0; i < length; i++) {
        h = 31 * h + getChar(value, i);
    }
    return h;
}

可想而知鸽斟,經(jīng)過這么一系列復雜的運算,再加上 JDK 作者這種大師級別的設計利诺,哈希沖突的概率我相信已經(jīng)降到了最低富蓄。

當然了,從理論上來說慢逾,對于兩個不同對象立倍,它們通過 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() 結果經(jīng)過 hash() 方法計算后有可能得到相同的結果,但這種概率微乎其微尽超,所以就導致 scores.get(new Student(18, "張三")) 無法得到預期的值 18官撼。

怎么解決這個問題呢?很簡單似谁,重寫 hashCode() 方法傲绣。

 @Override
 public int hashCode() {
     return Objects.hash(age, name);
 }

Objects 類的 hash() 方法可以針對不同數(shù)量的參數(shù)生成新的 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;
}

代碼似乎很簡單巩踏,歸納出的數(shù)學公式如下所示(n 為字符串長度)秃诵。

注意:31 是個奇質數(shù),不大不小塞琼,一般質數(shù)都非常適合哈希計算顷链,偶數(shù)相當于移位運算,容易溢出屈梁,造成數(shù)據(jù)信息丟失嗤练。

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

《Java 編程思想》這本圣經(jīng)中有一段話,對 hashCode() 方法進行了一段描述构哺。

設計 hashCode() 時最重要的因素就是:無論何時革答,對同一個對象調用 hashCode() 都應該生成同樣的值。如果在將一個對象用 put() 方法添加進 HashMap 時產(chǎn)生一個 hashCode() 值曙强,而用 get() 方法取出時卻產(chǎn)生了另外一個 hashCode() 值残拐,那么就無法重新取得該對象了。所以碟嘴,如果你的 hashCode() 方法依賴于對象中易變的數(shù)據(jù)溪食,用戶就要當心了,因為此數(shù)據(jù)發(fā)生變化時娜扇,hashCode() 就會生成一個不同的哈希值错沃,相當于產(chǎn)生了一個不同的鍵栅组。

也就是說,如果在重寫 hashCode()equals() 方法時枢析,對象中某個字段容易發(fā)生改變玉掸,那么最好舍棄這些字段,以免產(chǎn)生不可預期的結果醒叁。

好司浪。有了上面這些內(nèi)容作為基礎后,我們回頭再來看看本地方法 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 啟動時的一個全局參數(shù)认罩,可以通過它來切換哈希值的生成策略。

  • hashCode==0续捂,調用操作系統(tǒng) OS 的 random() 方法返回隨機數(shù)垦垂。
  • hashCode == 1,在 STW(stop-the-world)操作中牙瓢,這種策略通常用于同步方案中劫拗。利用對象地址進行計算,使用不經(jīng)常更新的隨機數(shù)(GVars.stw_random)參與其中矾克。
  • hashCode == 2页慷,使用返回 1,用于某些情況下的測試胁附。
  • hashCode == 3酒繁,從 0 開始計算哈希值,不是線程安全的控妻,多個線程可能會得到相同的哈希值州袒。
  • hashCode == 4,與創(chuàng)建對象的內(nèi)存位置有關弓候,原樣輸出郎哭。
  • hashCode == 5,默認值菇存,支持多線程夸研,使用了 Marsaglia 的 xor-shift 算法產(chǎn)生偽隨機數(shù)。所謂的 xor-shift 算法依鸥,簡單來說亥至,看起來就是一個移位寄存器,每次移入的位由寄存器中若干位取異或生成。所謂的偽隨機數(shù)抬闯,不是完全隨機的井辆,但是真隨機生成比較困難关筒,所以只要能通過一定的隨機數(shù)統(tǒng)計檢測溶握,就可以當作真隨機數(shù)來使用。

至于更深層次的挖掘蒸播,涉及到數(shù)學知識和物理知識睡榆,就不展開了。畢竟菜是原罪袍榆。

叨逼叨

二哥在 簡書 上寫了很多 Java 方面的系列文章胀屿,有 Java 核心語法、Java 集合框架包雀、Java IO宿崭、Java 并發(fā)編程、Java 虛擬機等才写,也算是體系完整了葡兑。

image

為了能幫助到更多的 Java 初學者,二哥把自己連載的《教妹學Java》開源到了 GitHub赞草,盡管只整理了 50 篇讹堤,發(fā)現(xiàn)字數(shù)已經(jīng)來到了 10 萬+,內(nèi)容更是沒得說厨疙,通俗易懂洲守、風趣幽默、圖文并茂沾凄。

GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java

如果有幫助的話梗醇,還請給二哥點個贊,這將是我繼續(xù)分享下去的最強動力撒蟀!

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末婴削,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子牙肝,更是在濱河造成了極大的恐慌唉俗,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件配椭,死亡現(xiàn)場離奇詭異虫溜,居然都是意外死亡,警方通過查閱死者的電腦和手機股缸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門衡楞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敦姻,你說我怎么就攤上這事瘾境∑缧樱” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵迷守,是天一觀的道長犬绒。 經(jīng)常有香客問我兑凿,道長凯力,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任礼华,我火速辦了婚禮咐鹤,結果婚禮上,老公的妹妹穿的比我還像新娘圣絮。我一直安慰自己祈惶,他們只是感情好,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布扮匠。 她就那樣靜靜地躺著捧请,像睡著了一般。 火紅的嫁衣襯著肌膚如雪餐禁。 梳的紋絲不亂的頭發(fā)上血久,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音帮非,去河邊找鬼氧吐。 笑死,一個胖子當著我的面吹牛末盔,可吹牛的內(nèi)容都是我干的筑舅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陨舱,長吁一口氣:“原來是場噩夢啊……” “哼翠拣!你這毒婦竟也來了?” 一聲冷哼從身側響起游盲,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤误墓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后益缎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谜慌,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年莺奔,在試婚紗的時候發(fā)現(xiàn)自己被綠了欣范。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖恼琼,靈堂內(nèi)的尸體忽然破棺而出妨蛹,到底是詐尸還是另有隱情,我是刑警寧澤晴竞,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布蛙卤,位于F島的核電站,受9級特大地震影響颓鲜,放射性物質發(fā)生泄漏表窘。R本人自食惡果不足惜典予,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一甜滨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瘤袖,春花似錦衣摩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至占婉,卻和暖如春泡嘴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逆济。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工酌予, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奖慌。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓抛虫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親简僧。 傳聞我的和親對象是個殘疾皇子建椰,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容