HashMap深刻學習

        HashMap 是Map接口的經常使用實現類而且 是以鍵值對(key,value)採用一種所謂的「Hash算法」,來決定每一個元素的存儲位置。程序員

HashMap的存儲實現    算法

    當程序試圖將多個 key-value 放入 HashMap 中時,以以下代碼片斷爲例: 數組

        HashMap map = new HashMap();性能

            map.put("語文",80.0);this

            map.put(「數學」,89.0);指針

            map.put(「英語」,78.2)code

當程序執行map.put(「語文」,80.0)時,系統將調用「語文」的hashCode方法獲得其HashCode值-每一個Java對象都有一個HashCode方法,均可以經過該方法得到它的hashCode值。獲得這個對象的hashCode值以後,系統會根據該hashCode值來決定元素的存儲位置。對象

    看一下HashMap類的put()方法源碼代碼以下。索引

    public V put(K key, V value) {接口

        //table 爲空則初始化table(Entry[])大小,和hash的掩碼值(須要的時候初始化)

    if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

//key值爲空則調用putForNullKey方法進行處理
        if (key == null)
            return putForNullKey(value);

        //根據key的keyCode計算Hash值
        int hash = hash(key);

    //搜索指定的hash值對應table中的索引
        int i = indexFor(hash, table.length);

//若是i索引處的Entry 不爲null,經過循環不斷遍歷e元素的下一個元素
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;

            //找到指定key與須要放入的key相等(hash值相等,經過equals比較返回true)
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

//若是i索引處的Entry爲null,代表此處尚未Entry 

        modCount++;

    //將key、value添加到i索引處
        addEntry(hash, key, value, i);
        return null;
    }

 

上面的源碼程序中用到了一個重要的內部接口 Map.Entry,每一個Map.Entry實際上是一個key-value對。當系統決定存儲HashMap的key-value對時,徹底沒有考慮Entry 中的value,而僅僅只是根據key來計算並決定每一個Entry的存儲位置。

 從上面put方法的源碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,首先根據key的hashcode()返回值決定該Entry的存儲對象的位置:若是兩個Entry的key的hashcode()返回值相同,那它們的存儲位置相同;若是這兩個Entry 的key經過equals比較返回true,新添加Entry的value將覆蓋集合中的原有的Entry的value,但key不會覆蓋;若是這兩個Entry 的key經過equals比較返回false,新添加的Entry將與集合中原有的Entry 造成Entry鏈,並且新添加的Entry位於Entry鏈的頭部-具體看AddEntry()方法說明

注意:當向HashMap中添加key-value對,由其key的hashcode()返回值決定該key-value對(Entry對象)的存儲位置。當兩個Entry對象的Key的hashcode()返回值相同時,將由key的equeals()比較值決定是採用覆蓋行爲(返回true執行),仍是產生Entry鏈(返回false執行)

上面程序中還調用了addEntry(hash,key,value,i);代碼,其中addEntry是hashMap提供的一個包的訪問權限的方法,該方法僅用於添加一個key-value對,下面是該方法的源碼

 void addEntry(int hash, K key, V value, int bucketIndex) {

    //若是Map中的key-value對數量超過了極限,當size>=threshold時,HashMap會自動調用resize方法擴充HashMap的容量。每擴充一次,hashMap就增大一倍。
        if ((size >= threshold) && (null != table[bucketIndex])) {

            //把table對象的長度擴充2倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

 

void createEntry(int hash, K key, V value, int bucketIndex) {

    //獲取指定bucketIndex索引處的Entry
        Entry e = table[bucketIndex];  //1

    //將建立的Entry彷彿bucketIndex索引處,並讓新的Entry指向原來的Entry
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

上面的源代碼很簡單,系統將新添加的Entry對象放入table數組的bucketIndex索引處,若是bucketIndex索引出已經有一個Entry對象,新添加的Entry對象指向原有的Entry對象(產生一個Entry鏈);若是bucketIndex索引處沒有Entry對象,也就是1處的e變量爲null,即新放入的Entry對象指向null,就沒有產生Entry鏈。(注意:在同一個bucketIndex存儲Entry鏈的狀況下,新放入的Entry老是位於bucketIndex索引中,而最先放入該bucketIndex索引位置的Entry則位於Entry鏈的最末端)

 size:改變量保存了該HashMap中全部包含key-value對的數量。

threshold:該變量包含了HashMap能容納的key-value對的極限,它的值等於HashMap容量乘以負載因子(DEFAULT_LOAD_FACTOR)

table是一個普通的數組,每一個數組都有一個固定的長度,這個數組的長度就是HashMap的容量。

jdk1.7之前,建立HashMap時,系統會自動建立一個table數組來保存HashMap 的Entry。jdk1.7中是當掉用put方法時纔對建立table數組。下面源碼:

jdk1.6HashMap構造方法

public HashMap(int initialCapacity, float loadFactor) {

        //初始容量不能爲負數
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);

        //若是初始容量大於最大容量,讓初始容量等於最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        //負載因子必須是大於0的數值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        //========如下方法是jdk1.7之前版本的內容。

    //計算出大於initialCapacity的最小的2的n次方值   

     int capacity =1;

        while(capacity<initialCapacity)

            capacity<<=1;

        this.loadFactor = loadFactor;

//    設置容量極限等於容量乘以負載因子

        threshold =(int)(capacity*loadFactory);

    table = new Entry[capacity];

//=========================

      this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

jdk1.7的put方法:

  public V put(K key, V value) {

    //若是table值爲空則建立
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

對於HashMap和子類而言,它們採用Hash算法來決定集合中元素的存儲位置。當系統開始初始化HashMap時(jdk1.7之前版本)或者調用put(key,value)(jdk1.7以上版本)方法時。系統會建立一個長度爲capacity的Entry數組。這個數組存儲元素的位置被稱爲「桶(bucket)」,每一個bucket都有指定的索引,系統能夠根據其索引快速訪問該bucket裏存儲的元素。

不管什麼時候,HashMap的每一個「桶」只能存儲一個元素(即一個Entry),因爲Entry對象包含一個引用變量(就是entry構造器的最後一個參數)用於指向下一個Entry,所以可能出現:HashMap的bucket中的只有一個Entry,但這個Entry指向另外一個Entry----造成一個Entry鏈。以下圖:

當HashMap的每一個bucket裏存儲的Entry只是單個Entry,即沒有經過指針產生Entry鏈時,此時的HashMap具備最好的性能。當程序經過key取出對應的value值時,系統只要先計算出該key的hashcode()返回值,在根據系統HashCode返回值找出該key在table數組中的索引,而後取出該索引出的Entry,最後返回該key對應的value。HashMap對應的get(K key)方法源碼以下:

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;
    }

 

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

代碼中能夠看出,若是hashMap的每一個bucket裏只有一個Entry,HashMap能夠根據索引快速的取出該bucket裏的Entry.在發生Entry衝突狀況下,單個bucket裏存儲的不是一個Entry,而是一個Entry鏈,系統只能按書序遍歷每一個Entry,直到直到想要的Entry爲止。若是下號要搜索的Entry位於該Entry鏈的末端(該Entry最先放入該bucket鏈,那麼系統必須循環到最後才能找到元素)。

總結:HashMap在底層將key-value當成一個總體進行處理。這個總體是一個Entry對象。HashMap底層採用一個Entry[]數組來保存全部的key-value對,當須要存儲一個Entry對象時,會根據Hash算法來決定其存儲位置;當須要取出一個Entry時,也會根據算法找到其存儲位置,直接取出該Entry。因而可知,HashMap之因此能快速存、取它所包含的Entry,徹底相似於現實生活中的:不一樣的東西放在不一樣的位置,須要時才能快速找到它。

當建立HashMap時,有一個默認的負載因子(load factor),其默認值爲0.75.這是時間和空間成本的一種折中;增大負載因子能夠減小Hash表(減小Entry數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的操做(HashMap 的get()與put()方法都要用到查詢);減少負載因子會提升數據的查詢性能,但會下降Hash表所佔的內存空間。

掌握了上面的知識。能夠在建立HashMap的時候根據實際狀況適當地調整load factor的值。。若是程序比較關係內存的開銷。內存比較緊張,能夠適當的增長負載因子;若是程序比較關心時間開銷,內存比較寬裕,則能夠適當地減小負載因子,一般狀況下。程序員不須要改變負載因子的值

相關文章
相關標籤/搜索