一個算法揭示HashMap的原理

大概是因為世界杯讓我有借口去偷懶的原因吧,最近一段時間在公司也在做java的事情,所以沒有太多的心思去寫文章。萬惡始于世界杯,也終于世界杯,以后就沒有偷懶的借口了,心碎。。。

好吧,我來說說今天要講的內容,源于一個大學的大神,突然來問我一個很簡單的算法,然后就引起了自己的一些思考,下面我們來看看這個算法:

image

很簡單,我們創建了兩個Set,都知道Set是保存不重復的屬性,然后第一個創建了100000個10000之內的隨機數,第二個Set創建了190002個1999-11999之間的隨機數。

看上去很拗口,但是問題很簡單。就是算出兩個Set里面相同的數總共多少個。
拿到問題了大家估計都會想,哇,這個問題很簡單,兩個for循環就完事了:

        Random random = new Random();
        int z = 0;

        Set<Integer> list1 = new HashSet<>();
        for (int i = 0; i < 100000; i++) {
            list1.add(random.nextInt(10000));
        }

        Set<Integer> list2 = new HashSet<>();
        for (int i = 9998; i < 200000; i++) {
            list2.add(random.nextInt(10000) + 1999);
        }
        for (int str : list1) {
            for(int str1 : list2){
                if(str == str1){
                    z++;
                }
            }
        }

        System.out.println(z);

看上去很簡單,好吧,我們用Junit試試運行的結果:

image

625ms看上去也沒什么問題,但是,我的同學看到就開始調侃我了,你怎么那么菜,#¥%¥&……%#¥%¥……¥%……%¥%%¥%,此處省略1000字被調侃的話。然后他就給我看了他的運行結果:

image

我看到之后,有點驚呆了的意思,27ms跟625ms差了二十多倍的速度。這個時候不是我驚訝的時候,我知道,我要做的是去跪舔。。。去試探點口風。想不到這位同學竟然告訴我這是我每天都會用的東西,就能達到這個效果-----HashMap

那我趕緊去試試,確實:

        Random random = new Random();
        int z = 0;
        Set<Integer> list1 = new HashSet<>();
        for (int i = 0; i < 100000; i++) {
            list1.add(random.nextInt(10000));
        }

        Set<Integer> list2 = new HashSet<>();
        for (int i = 9998; i < 200000; i++) {
            list2.add(random.nextInt(10000) + 1999);
        }

        HashMap<Integer,Integer> hashMap=new HashMap();
        for (int str : list1) {
            hashMap.put(str,str);
        }

        for(int str : list2){
            if(hashMap.get(str) != null){
                z++;
            }
        }

        System.out.println(z);

很神奇的是,直接用Hashmap直接就能得到這樣一個結果。然后我親愛的同學又來一頓調侃,hashmap的原理都不懂你就!¥%¥……%¥@%#@%¥%@%¥。然后我就只好認慫的看一下總結了一下hashmap的原理。

關于hashmap,我們首先得說說哈希表的基本概念,我還去翻了一下自己的大學課本,以下是官方的回答:

哈希表的基本概念

哈希表又稱散列表,是除了順序表存儲結構、鏈表存儲結構和索引表存儲結構之外的又一種存儲線性表的存儲結構。哈希表存儲的基本思路是:設要存儲的對象個數為n,設置一個長度為m(m>=n)的連續空間以線性表中每個對象的關鍵字K下標i(0<=i<=n-1)為自變量,通過一個稱為哈希函數的函數h(K下標i),把K下標i映射為內存單元的地址h(K下標i),并把該對象存儲在這個內存單元中。h(K下標i)也稱為哈希地址(又稱為散列地址)。把如此構造的線性表存儲結構稱為哈希表。

那下面我用一幅圖概括上面的一大堆文字:

image

這就很簡單明了了吧~

看懂了哈希表的基本概念,那下面我們就要講講用它的概念做出來的偉大作品HashMap了:

HashMap是什么:

HashMap是Java常用的用來儲存鍵值對的數據結構,它是線程不安全的,可以儲存null鍵值。

HashMap實現原理:

其實HashMap主干是一個數組叫Note:

transient Node<K,V>[] table;

而Node是HashMap里面的一個靜態類:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//對key的hashcode值進行hash運算后得到的值,存儲在Note,避免重復計算
        final K key;
        V value;
        Node<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        ...
}        

看到上面代碼Node里面的next,是一個單鏈表結構,而結合table是一個Node數組,可以判斷出HashMap是一個數組加鏈表的結構。下面我總結一下HashMap的結構圖:

image

好吧,我們每天都用的HashMap大概就是上圖數組+鏈表的結構。那突然我想到個問題,為什么HashMap偏偏采用的是數組+鏈表的結構呢?

哈希沖突

答案其實很簡單,那就是哈希沖突,因為上面說到哈希函數h(K),里面是一個算法,當兩個不同的元素,通過哈希函數的算法算出的實際存儲地址是相同的時候,就會出現沖突,這個沖突就稱為哈希沖突。所以,當實際存儲地址相同(可以看做HashMap存儲的數組)的時候,就需要鏈表來存儲擁有相同實際地址但幾個不同的元素。這種稱為鏈地址法,也就是數組+鏈表的方式,可以很好的解決哈希沖突。

除此之外,我們也可以明白為什么HashMap效率能這么高了,對于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對于添加操作,其時間復雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對于查找操作來講,仍需遍歷鏈表,然后通過key對象的equals方法逐一比對查找。當然,HashMap中的鏈表出現越少,也就是哈希沖突出現越少的話,性能才會越好。

源碼剖析

下面我們繼續分析HashMap的源碼:

//存儲的key-value鍵值對的個數
transient int size;
//負載因子,代表了table的填充度有多少,默認是0.75
final float loadFactor;
//閾值,當table == {}時,該值為初始容量(初始容量默認為16);當table被填充了,也就是為table分配內存空間后,threshold一般為 capacity*loadFactory。
int threshold;

HashMap的容量是否需要擴充,看的就是threshold,capacity和loadFactory這三個值。舉個例子,如果初識容量是16(即capacity為16,且loadFactory為0.75)的時候:

    threshold = capacity * loadFactory
              = 12

也就是說當前HashMap的容量閥值為12,當我們實際的容量超過12的時候,HashMap會進行擴容,從16會變成下個2的n次冪,也即32.
下面我們來分析下幾個方法:

put

    public V put(K key, V value) {
        //(1)
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //(2)判斷table是否為空,是就執行resize()方法
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果在數組里面沒找到相應的位置則需要新建一個位置    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //否則,在數組里面找到位置若找到相同值,則覆蓋;若是不同的值
            //則需要在鏈表上新建一個結點。
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

代碼很長,但我們主需要捉住關鍵代碼分析即可
注意點(1):首先我們看看hash()方法做了什么事:

    //通過這個方法來獲取元素要存儲的位置
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

下面我們看看注意點(2):resize()方法做了什么:

    //HashMap可設置的最大容量為2的30次冪
    static final int MAXIMUM_CAPACITY = 1 << 30;

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //當容量大于等于可設置的最大容量時,取原來的值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //當原來的容量大于等于原來默認容量值時,容量翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using 
            ...
        }
        return newTab;
    }

我們可以看到resize()就是一個對容量進行計算擴充的方法。
每次當原來的值不夠用的時候,容量就會翻倍。
有人會問,為什么擴充的容量一定是要2的n次冪呢?
下面我就講講這個運算,大家是否還記得putVal()方法中有這么一段
(n - 1) & hash,是一個與運算。

關于這個運算,其實就是確定得到最終數組索引位置。
我們用同樣的二進制數去做分別跟31,和另一個2的n次冪數做運算

(1)
1 0 1 0 1
&
1 1 1 1 0 = 1 0 1 0 0
(2)
1 0 1 0 0
&
1 1 1 1 0 = 1 0 1 0 0
(3)
1 0 1 0 1
&
1 1 1 1 1 = 1 0 1 0 1

結果很明顯,分別從(1)(2)(3)看出,當與運算為2的n次冪做與運算時,得到的結果會變得更多。得到的結果更多意味著最終數組索引位置變化也會更多,最后得到的哈希沖突就會變少。所以這里大家就明白為什么擴容的大小一定要是2的n次冪了吧

還有一個問題,有人會問為什么用與運算可以確定得到最終數組索引位置呢?好吧,我再來舉個例子,我們用一個很大的數和很小的數分別跟2的5次冪做與運算:
比32要大的數:

1 1 1 1 1 1 1 1 1
&
0 0 0 0 1 1 1 1 1 = 1 1 1 1 1

比32要小的數:

0 0 0 0 0 0 0 1 1
&
0 0 0 0 1 1 1 1 1 = 0 0 0 1 1

上面的結果就看到,無論多大或者多小的數跟32做與運算,得出的結果都是小于等于32的,那由此大家就知道當我們的HashMap長度為32的時候,無論hash()方法算出來的數是多少,跟長度做與運算,最后得出的最終位置肯定比HashMap的長度范圍內。

get

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果數組的第一個值相同,則返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //若數組的第一個值沒找到,則需要向該數組里的鏈表逐個結點查找    
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

我們從get方法就能看到,如果查詢的時候,能第一次查出值,效率是很高的,但是如果有哈希沖突,需要往鏈表下面查,效率就會變慢。

好了,HashMap的原理大概就是這樣了,對于HashMap的原理面試的時候應該也沒被少問,所以自己也參考網上的資料,總結了自己的一些自己的看法。其實大神還告訴我,這是一種代碼中空間換時間的做法,我們多創建了一個HashMap的空間,卻時間少花了二十多倍的運算時間,何樂而不為?代碼中或許還有很多很好的例子等著我們去發掘。

很久沒寫文章,如果有寫錯的地方,請多多指正,謝謝大家!

我的掘金:
https://juejin.im/user/594e8e9a5188250d7b4cd875/posts
我的簡書:
http://www.lxweimin.com/u/b538ca57f640

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容

  • HashMap 是 Java 面試必考的知識點,面試官從這個小知識點就可以了解我們對 Java 基礎的掌握程度。網...
    野狗子嗷嗷嗷閱讀 6,678評論 9 107
  • HashMap 源碼分析 前幾篇分析了 ArrayList , LinkedList ,Vector ,Stack...
    醒著的碼者閱讀 2,855評論 4 44
  • 恍恍惚惚,18年已過一個月。作為嚴重的拖延癥患者,近兩周才對17年做了整體的回顧,做了組內的績效溝通,今天也抽空做...
    數大招瘋閱讀 196評論 0 1
  • 領導不在的日子我比想象中更加焦頭爛額,要寫材料需要引用市長那么一段批示,認不出連體子有一搭無一搭發給了爸爸。 他去...
    小記記閱讀 209評論 0 0
  • 此刻,只想回到杭州,回到那個場域,揭開蓋子,自己的性格是那樣的不堪,只到今天越來越無法面對這么討厭的自己。回...
    SmileV_7e84閱讀 260評論 0 8