HashMap源碼分析底層原理

 

HashMap原理

先以一個簡單的例子來理解hashmap的原理。在Java中先隨機產生一個大小爲20的數組如下:

這裏寫圖片描述

hash表的大小爲7,將上面數組的元素,按mod 7分類如下圖:

這裏寫圖片描述

將這些點插入到hashmap中(簡單hashmap)後如下圖:

這裏寫圖片描述 
由上圖可知: 
① hashmap是用鏈地址法進行處理,多個key 對應於表中的一個索引位置的時候進行鏈地址處理,hashmap其實就是一個數組+鏈表的形式。

② 當有多個key的值相同時,hashmap中只保存具有相同key的一個節點,也就是說相同key的節點會進行覆蓋。

③在hashmap中查找一個值,需要兩次定位,先找到元素在數組的位置的鏈表上,然後在鏈表上查找,在HashMap中的第一次定位是由hash值確定的,第二次定位由key和hash值確定。

④節點在找到所在的鏈後,插入鏈中是採用的是頭插法,也就是新節點都插在鏈表的頭部。

⑤在hashmap中上圖左邊綠色的數組中也存放元素,新節點都是放在左邊的table中的,這個在上圖中爲了形象的表現鏈表形式而沒有使用。

HashMap

上面只是簡單的模擬了hashmap 真實的hashmap的基本思想和上面是一樣的不過更加複雜。HashMap中的一個節點是一個Entity 類如下圖:

這裏寫圖片描述

Entry是HashMap的內部類 包含四個值(next,key,value,hash),其中next是一個指向 Entry的指針,key相當於上面節點的值 value對應要保存的值,hash值由key產生,hashmap中要找到某個元素,需要根據hash值來求得對應數組中的位置,然後在由key來在鏈表中找Entry的位置。HashMap中的一切操作都是以Entry爲基礎進行的。HashMap的重點在於如何處理Entry。因此HashMap中的操作大部分都是調用Entry中的方法。可以說HashMap類本身只是提供了一個數組,和對Entry類中方法的一些封裝。

下面從源碼方面對 HashMap進行解析:

①HashMap的繼承關係

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

從上面可以看到HashMap繼承了AbstractMap , 並且實現了Cloneable, Serializable 接口。

②HashMap的構造函數

下面的代碼都是經過簡化處理的代碼,基本流程不變只是爲了更好的理解修改和刪除了一部分內容

public HashMap(int initialCapacity, float loadFactor) {

/*initialCapacity 初始化hashmap中table表的大小,前面的圖中左邊綠色部分的數組就是table。loadFactor填裝因子。
*/
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
  //如果初始化大小小於0,拋出異常                                          
        if (initialCapacity > 2^30)
            initialCapacity = 2^30;
 //HashMap 中table的最大值爲2^30。         

        /* 生成一個比initialCapacity小的最大的2的n次方的值,這個值就是table的大小。table就是一個Entry類型的數組。
        */
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        table = new Entry[capacity];
//新建一個Entry類型的數組,就是前面圖中左邊的數組。不過數組的元素是Entry類型的。       
    }

上面的代碼要做幾點說明:

①填裝因子:loadFactor 表示填裝因子的大小,簡單的介紹一下填裝因子:假設數組大小爲20,每個放到數組中的元素mod 17,所有元素取模後放的位置是(0–16) 此時填裝因子的大小爲 17/20 ,裝填因子就爲0.85啦,你裝填因子越小,說明你備用的內存空間越多,裝填因子的選定,可以影響衝突的產生,裝填因子越小,衝突越小。

②HashMap初始化過程就是新建一個大小爲capacity,類型爲Entry的數組,Entry上面已經介紹過這個類,包含一個指針一個key,一個value,和一個hash。capacity是2的次冪,至於爲什麼是2的次冪後面會有介紹的。

下面是另外兩個構造函數

 public HashMap(int initialCapacity) {
        HashMap(initialCapacity, 0.75);
        //調用了上面的構造函數,只不過使用了默認的填裝因子0.75
    }

public HashMap() {
        HashMap(16, 0.75);
        //生成一個table大小爲16,填裝因子0.75的HashMap
    }

③由上可知如果用戶直接使用HashMap()構造函數來new一個HashMap 會生成一個大小爲16,填裝因子爲0.75的 HashMap。

③HashMap中的put(key,value)函數

還是先上源碼

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
/*如果key爲null則調用  putForNullKey(value) 函數 這個函數先在table[0]這條鏈上找有沒有key 爲null的元素如果有就覆蓋,如果沒有就新建一個new一個key爲null,value=value hash=0,的Entry放在table[0]。
*/     
        int hash = hash(key);
//獲得key的hash值                        
        int i = indexFor(hash, table.length);
//由hash值確定放在table表中的那一條鏈上。類似於取模後放在數組中的哪個位置。        
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
     //如果鏈上原來有一個hash值相同,且key相同的則用新的value值進行覆蓋。           
            }
        }
//否則利用hash,key,value,new一個Entry對象插入到鏈表中。
        modCount++;
        addEntry(hash, key, value, i);
        return null;

    }

對上面代碼做幾點說明:

① HashMap中的key可以爲 null ,此時hash=0,爲什麼key可以爲null,因爲HashMap中放的元素是Entry,而Entry包含了4個值(key,value,hash,next),key爲 null 時不影響Entry映射到HashMap中。

②hash(key),產生一個正整數,這個整數與key相關。這個hash(key)函數比較關鍵,後面會進行說明。

③用戶插入的(key,value)對不是直接放到HashMap中的,而是用(key,value)以及後面由key value產生的hash,new一個Entry對象後再插入到HashMap中的。

④如果對應的鏈上有一個hash值個key相同的Entry則覆蓋value值,不new Entry對象,如果沒有會先new 一個對象在將其插到對應的鏈上。(其中可能會涉及到擴充HashMap)。

下面看看hash(key)函數

final int hash(Object k) {
        int h = 0;
        h ^= k.hashCode(); 
        //hashCode 返回一個整數值,這個值跟對象有關,不同對象的hashCode值一般不同。       
        h ^= (h >>> 20) ^ (h >>> 12);

        return h ^ (h >>> 7) ^ (h >>> 4);

    }

hash函數的作用是使hashmap裏面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。

④HashMap中的get(Object key)函數

上源碼

public V get(Object key) {
        if (key == null)
            return getForNullKey();
//如果key==null則在table[0]這條鏈上找,如果找到返回value值,否則返回null ,因爲key==null的都是放在table[0]這條鏈上的。          
        Entry<K,V> entry = getEntry(key);
//  getEntry(key)先key的hash值找到在數組的哪條鏈上,然後在鏈上查找key相同的如果沒找到返回null    
//如果找到了返回Entry的value值。  
        return null == entry ? null : entry.getValue();        
    }

private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

   Entry<K,V> getEntry(Object key) {
        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;
    }
  • 1

上面代碼的幾點說明

① 通過key來鏈表中查找元素包括兩個過程,先由hash找到鏈(hash由key產生,不同的key可能產生相同的hash值,相同的hash值放在同一條鏈上),再用key在鏈上找。

② 如果key爲null則只在table[0]和其鏈上查找,因爲key爲null都放在table[0]及其鏈上了。

③因爲在HashMap中查找到的是Entry對象,返回的值是Entry對象的value值。

重點Entry類

其實理解HashMap最重要的在於理解Entry類,Entry類相當於鏈表中的一個節點,是HashMap操作的基礎。下面主要從Entry類的幾個方法來理解Entry類和HashMap的關係。

①Entry中的addEntry( hash, key, value, bucketIndex)函數

在HashMap中調用put(key,value)時,如果(key,value)是首次加入到HashMap中,就會調用 
addEntry( hash, key, value, bucketIndex)函數,將其加入到table表對應的位置中(注意是table中,不是後面的鏈中,首次加入的元素都是採用的頭插法)。下面是源碼:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            //如果size的值超過了threshold,將table擴容兩倍
            hash = (null != key) ? hash(key) : 0;
            //如果key爲null則hash=0,否則hash函數利用key來產生hash值。
            bucketIndex = indexFor(hash, table.length);
            //bucketIndex就相當於取模後對應的table表中的哪個位置。
        }
        //如果不存在容量不夠問題則直接新建一個Entry對象。       
        createEntry(hash, key, value, bucketIndex);

    }


void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //獲得原來首位的Entry對象
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //將新建的Entry對象放在鏈表的首位,然後用next指向原來放在首位的對象。也就是頭插。
        size++;
    }

上面代碼的幾點說明: 
①bucketIndex是由hash取模後對應於table表中的哪個位置。indexFor(hash, table.length)其實是一個取模函數。它的實現很簡單 hash& (length-1),就是用hash值與上table表的長度減1。

②並不是對每一個(key,value)對都產生一個Entry對象,只是(key,value)對首次放到HashMap中時,或者HashMap中沒有相同的key時,才產生一個Entry對象,否則如果有相同的key則會直接將value值賦個Entry的value。

③新產生的Entry都是放在了table中,也就是鏈表的首位,採用鏈表的頭插法。

②HashMap中的keySet()函數

作用:返回HashMap中key的集合。keySet是HashMap中的內部類:

public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
        //如果keyset爲null就產生一個keyset對象。
    }

    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
            //newKeyIterator迭代器用於遍歷key。
        }
        public int size() {
            return size;
            //返回keyset的大小
        }
        public boolean contains(Object o) {
            return containsKey(o);
            //是否包含某個key
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
            //移除某個key的Entry。
        }
        public void clear() {
            HashMap.this.clear();

        }
    }

keySet是用來遍歷整個HashMap的,因此是十分重要的,下面做幾點說明。 
①keyset中有一個迭代器可以迭代的獲取下一個key的值,通過key的值就可以獲得Entry對象了。

②對應key的迭代遍歷是table表中由左向右,由上向下進行的,也就是先遍歷table[0]這條鏈上的,然後遍歷table[1]這條鏈上的依次往下進行。

③newKeyIterator具體實現這裏就不多介紹,只要知道上面的功能怎麼實現就可以了。

下面是利用keyset來實現遍歷HashMap的例子:

        HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>();      
        for(int i=0;i<20;i++)
        {
            hashMap.put(i, i+1);
        }
        //新建一個hashmap往裏面放入20個(key,value)對。
        Iterator<Integer> iterator= (Iterator<Integer>) hashMap.keySet().iterator();
        //獲得keyset的iterator,進行遍歷整個hashmap。
        while(iterator.hasNext())
        {
            Integer key=(Integer) iterator.next();
            Integer val=(Integer)hashMap.get(key);
            System.out.println(key+": "+val);
        }

②HashMap中的entrySet()函數

作用:返回HashMap中Entry的集合。對於entrySet這裏就不上源碼了,舉一個使用entrySet遍歷HashMap的例子:

HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>();      
        for(int i=0;i<20;i++)
        {
            hashMap.put(i, i+1);
        }
        Iterator<Entry<Integer, Integer>> iterator=hashMap.entrySet().iterator();
        while(iterator.hasNext())
        {
            Entry  entry= iterator.next();

            System.out.println(entry.getKey()+": "+entry.getValue());
        }

使用entrySet()函數遍歷比keySet()函數遍歷快,因爲keySet()函數是先通過entrySet()求出key然後在通過key來遍歷獲得Entry的,所以速度比entrySet()慢很多。 

hash(散列)衝突問題解決

  • 拉鍊法:hashMap採用
  • 線性探測法:實例ThreadLocal類中ThreadLocalMap採用的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章