HashMap的源碼理解

HashMap是常用的集合。採用鍵值對方式存儲.   此博客是基於jdk1.8分析的。

  一:先看看HashMap的繼承關係:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

   1.繼承AbstractMap,然後實現了Map.其實AbstractMap也是實現了大部分的Map方法,其他Map的實現類只需要繼承       AbstractMap然後實現少量的方法即可。 

   2.實現Cloneable可以被克隆,實現Serializable接口可以被序列化。

二:說下HashMap的大致框架。

     HashMap的主幹是一個Map.Entry<K,V>數組(jdk1.8和jdk1.7形式上有些不同,jdk1.8採用Node)。

transient Node<K,V>[] table;

Map.Entry<K,V>對象:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash值
        final K key;//節點的key值
        V value;//節點的value值
        Node<K,V> next;//此節點連接的下一個節點(解決hash衝突)
    //省略部分代碼,下面的代碼是 get,set方法,equals以及toString方法
}

然後根據hash算法來確定每  個Map.Entry對象存放的位置。如果不同的對象的hash值一樣,這就造成了hash衝突(不能將多個對象放置在同一個數組位置),此時可以採用鏈表結構,即相同的hash值的Map.Entry對象接在該Map.Entry對象後面。

整體結構(借用下大神的圖):

三:幾個關鍵參數以及構造方法。

//默認的初始容量 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;//1073741824
//默認的填充因子
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Node<K,V>[] table;//HashMap中的'主幹數組'

transient int size;//HashMap中實際存儲了多少個對象

transient int modCount;//HashMap對象結構修改的次數,包括增,刪。用於fail-fast機制。和ArrayList 
                       //LinkedList類似。

 int threshold;//最開始是用作初始化HashMap的數組,後面用來判斷是否擴容。
               //threshold=capacity * loadfactor  即容量*填充因子

final float loadFactor;//實際的填充因子參數

構造方法:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;//容量如果超過最大的就採用最大默認容量
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;//賦值填充因子
        this.threshold = tableSizeFor(initialCapacity);//返回一個2的冪次方數,便於hash算法
    }

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
//由於構造方法沒有給‘主幹數組’賦值,即此時該HashMap還沒有完全初始化。真正給map的數組賦值是在
//put方法中.下面給予介紹

 

四:介紹常用的方法,put(K k,V v)

  put(K k,V v).先說下大概流程吧:首先hash(k)來獲取hash值。然後通過(n-1)&hash值得到Map數組的下標。將該值放置在該地方。如果已經存在值了,就接在該值後面。

代碼:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

然後看看 hash方法:

static final int hash(Object key) {
        int h;
      //key 爲null的hash值爲0 
      //調用key 類型的hashCode方法 然後和該(hashCode值無符號右移16位後)進行或運算
      //二進制運算了解:https://blog.csdn.net/echohuangshihuxue/article/details/86182908
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

再看看核心邏輯 putVal:其中求數組下標:hash&(n-1)。本質上和hash%n 算法是一樣的。

   1.hash%(n-1)很好理解。即得到的數據爲(0 到n-1之間)。即數組的下標。不會越界。

   2.hash&(n-1)的效率比hash%n的效率高。hash%n最終還是要轉換爲二進制進行計算。

   3.爲啥hash&(n-1)和 hash%n一樣呢。二進制可以參考:https://blog.csdn.net/echohuangshihuxue/article/details/86182908

    首先的理解&運算。即都爲1才爲1.而n是2的冪次方。二進制數形式爲: 0000....1..0000.

                                                                                  n-1的二進制形式爲: 0000....0..1111.

   即hash&(n-1)的結果爲                                          0000.....0..1111   

                                                                                         &

                                                                                 0101.....1..0101

其結果必定小於等於n-1.如果(n-1)和hash相等,那麼&的結果就是n-1.其他情況都小於n-1.並且n-1的二進制有效位全是1.所以

和hash進行&運算。前面的全是0.後面的結果,hash的結果是啥 &的結果就是啥。因爲0或者1與1進行&運算。結果有前面的數決定。所以n-1與hash值進行&運算得的結果爲  hash-(Math.floor(hash/n))  *n.其中帶紅色的運算是取 hash/n的最接近的整數。比如hash/n 爲2.1. 那麼整個值爲2. 其實上面的表達式就是   hash%n了。

 

可能我表達的不太好。別人還沒有理解。最開始我就是自己做了很多測試。最終發現確實是這樣的      

/**
	 * 前提是s爲2的冪次方。
	 * 
	 * 測試 one%s  和 one &(s-1) 是等值的
	 * 經測試,確實是等值的
	 */
	@Test
	public void testOne(){
		int one=187832123;
		int s=2<<4;//將2的冪次方 進行左移或者右移 得到的依然是2的冪次方    
		           //二進制可以參考:https://blog.csdn.net/echohuangshihuxue/article/details/86182908
		int res_one=one%s;
		
		int res_two=one&(s-1);
		System.out.println(res_one);//27
		System.out.println(res_two);//27
	}
	

putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//沒有對下面的table進行操作,基本都是用tab進 
                                               //行替代操作。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//如果數組爲空,resize()即給Map的數組賦值。真正完成 
                                         //初始化。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//如果沒有發生hash衝突,直接插入
        else {
            Node<K,V> e; K k;                       //如果發生了hash衝突,即接在後面,修改節 
                                                    //點的next
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)    //關於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) { // .如果已存在了key.那麼覆蓋,返 
                             //回舊的值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;               //增加了修改次數,用友 fail-fast機制
        if (++size > threshold)
            resize();             //進行擴容操作
        afterNodeInsertion(evict);//屬於節點爲TreeNode類型(需要修改結構)。現在不考慮
        return null;
    }

五:常用方法,get(Object key)

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

hash方法之前看過了。主要邏輯就在getNode裏面。

 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 &&
            //用hash值 和(n-1)進行&運算  的值作爲Map的數組下標值
            (first = tab[(n - 1) & hash]) != null) { //如果存在。
            if (first.hash == hash &&  //首先比較第一個 通過equals方法
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {//如果第一個比較失敗,然後比較下一個
                if (first instanceof TreeNode)//Node對象爲TreeNode類型暫不考慮
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do { //遍歷這個數組下標下的節點。找到返回,沒有返回null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

 

總結: 1.HashMap的get,即查找方法。是首先根據hash()方法計算hash值,然後用(n-1)&hash值得到HashMap主數組的下                    標,  這裏的n爲數組的長度。 然後在該數組對應的地方進行equals方法比對鏈表值(Node).如果equals方法相等,則證               明找到。

           所以我們重寫HashMap的hash方法是,一定得重寫equals方法。並且保證:key的hash值相等時,equals的值必須爲                   true;

         2.HashMap的key不能重複。因爲在put方法中,首先根據key的hash方法得到hash值,再根據(n-1)&hash來查找數組的                 下標。如果key值一樣,那麼其hash值一樣,並且equals方法爲true.最終會進行覆蓋。所以不會存在相同的key值。

         3.如何理解hashMap的效率高:

           比如集合中存了1000個對象。LInkedList的集合的get(int one)方法,需要用equals方法一個個去比對。可能比較500次(可以看LinkedList源碼)。  而HashMap不同,hashMap首先會計算hash值。找到對應的數組下標。如果該下標下有50個對象。則只需用equals方法比較50次。數組根據下標求值消耗忽略不算。  如果hash算法以及填充因子(默認0.75)設置的好。效率會更高。

        4.後續再看看爲hash()方法。

 

 

 

 

 

 

 

 

 

 

 

 

下面的部分雜談,哈哈。

一:可能你用hashMap的時候,就是Map<String,Object> map=new HashMap<String,Object>();這樣創建一個Map對象,然後使用map.put(),然後,map.get()。其實我們我可以進一步想想,map.put(one,two)方法是如何判斷one的唯一性的(即one不能重複)。

一個最原始的辦法,就是拿這個one去和map的key一個個去比較,一般是通過equals比較。如果是數量很多的話,可能你put一個元素都要很大的開銷。同樣的道理,你get一個元素也要花很長的時間。所以就需要算法來優化了。

二:那HashMap是怎麼優化的呢。這裏我用自己的話儘量說通俗點吧。

    首先HashMap在內存分爲m個部分,用int(散列碼)數組標識arr。比如arr[x]代表a部分。每個部分存儲着若干個key.

   現在,當我們put一個元素時,此時key對象會調用自己的hashcode方法,生成一個int數字,該int數字即匹配到上面所說的數組的下標,然後就找到對應的內存區域(如果匹配不上,即hashcode生成的int數字對應的數組中內存沒有對象,就把該key,value put進去),此時key就和該部分的對象進行equals比對。如果返回true,說明key對象存在,不能put進去。否則就Put進去。

  這裏說簡單說下,原來需要用key對象和hashMap中所有的對象比較,現在只需比較一部分。比如HashMap中有10000個對象,原來需要比較10000,現在就只需比較50次,中間的數組底層會有些消耗,但這樣還是會有很大的性能提升。

三:說到這裏,可能有點迷糊。打個不恰當的比方:現在這個書房有10000本書,你需要知道這個書房中是否有笑傲江湖這本小說。如果有,就不把這本書放進去,如果沒有,就將這本書放進去。如果書是毫無頭緒放着,那麼你就只能一本一本去看找了。現在如果你對書進行了整理分類,都貼了標籤。你就可以直接找到小說類。然後一本本比較就得了。是不是快多了。。。。。。

   其實標籤就好比HashMap中的數組。現在重點來了,你怎麼知道這本書是小說類?這就提到 了hash算法了。

四:其實hashCode和equals方法是Object就有的,說明任何對象都可以重寫hashCode和equals方法。說明設計者早就想好了,夥計們,當你需要 將對象(我指的是key值)put到HashMap中時, 就要將該對象重寫hashCode和equals方法。不然HashMap的key唯一性就會出錯。現在大夥可能更加迷糊了。我明明沒有重寫什麼hashcode和equals方法,怎麼在使用HashMap的時候,什麼問題都沒有呢,哈哈,其實我們一一般是將String或者Integer對象作爲key.而這兩個類已經重寫了hashCode和equals方法了。所以當然沒問題了。說了這麼多,好像跑題了。

五:迴歸到正題,如何在大量數據裏判斷是否有對象one,HashMap的做法是首先根據hashCode方法判斷(靠hashCode方法生成散      列碼,和底層的數字查詢該內存是否有對象。如果存在,就再採取equals方法比較),這裏還涉及到一個邏輯問題。就是一個對象和HashMap對象的equals方法爲true時,hashcode得到的散列碼一定在底層數組中有對象。反過來不一定對(通俗點說就是這個房間有一本書叫笑傲江湖,那麼這個書房一定會有個地方是放小說的)。

六.來看下HashMap的get方法。

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))

                return e;
        }
        return null;
    }

首先是調用hash(實際上是調用key對象的hashCode方法)。比較hash值。當hash值不相等,就不用比較equasl方法了,當hash值相等且equals爲true,纔會得到value。大概意思就是這樣。如果要深究,可以讀讀Thinking in java--容器深入研究(21章)。感興趣的還可以看看String的hashCode方法。

  public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

至於爲什麼h=31*h+val[i]這裏是31,這就是數學上的問題了。就到這裏。如有不對,多謝指正。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章