JDK源碼學習系列08----HashMap

                                                          JDK源碼學習系列08----HashMap

1.HashMap簡介

HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。
HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。
HashMap 的實現不是同步的,這意味着它不是線程安全的。它的key、value都可以爲null。此外,HashMap中的映射不是有序的

<span style="font-size:10px;">public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable</span>

Map接口定義了所有Map子類必須實現的方法。Map接口中還定義了一個內部接口Entry(爲什麼要弄成內部接口?改天還要學習學習)。Entry將在後面有詳細的介紹。

    AbstractMap也實現了Map接口,並且提供了兩個實現Entry的內部類:SimpleEntry和SimpleImmutableEntry。

2.HashMap的數據結構

Java最基本的數據結構有數組和鏈表。數組的特點是空間連續(大小固定)、尋址迅速,但是插入和刪除時需要移動元素,所以查詢快,增加刪除慢。鏈表恰好相反,可動態增加或減少空間以適應新增和刪除元素,但查找時只能順着一個個節點查找,所以增加刪除快,查找慢。有沒有一種結構綜合了數組和鏈表的優點呢?當然有,那就是哈希表(雖說是綜合優點,但實際上查找肯定沒有數組快,插入刪除沒有鏈表快,一種折中的方式吧)。一般採用拉鍊法實現哈希表。


ps:圖片來源於網絡

3.HashMap成員變量

HashMap 的實例有兩個參數影響其性能:“初始容量” 和 “加載因子”。容量 是哈希表中桶的數量,初始容量 只是哈希表在創建時的容量。加載因子 是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
通常,默認加載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以加載因子,則不會發生 rehash 操作。

    /**
     * 默認的初始容量,必須是2的冪。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    /**
     * 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 默認裝載因子 
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 存儲數據的Entry數組,長度是2的冪。 
     */
    transient Entry[] table;
    /**
     * map中保存的鍵值對的數量
     */
    transient int size;
    /**
     * 需要調整大小的極限值(容量*裝載因子)
     */
    int threshold;
    /**
     *裝載因子
     */
    final float loadFactor;
    /**
     * map結構被改變的次數
     */
    transient volatile int modCount;
HashMap是通過"拉鍊法"實現的哈希表。

它包括幾個重要的成員變量:tablesizethresholdloadFactormodCount
  table是一個Entry[]數組類型,而Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。 
  size是HashMap的大小,它是HashMap保存的鍵值對的數量。 
  threshold是HashMap的閾值,用於判斷是否需要調整HashMap的容量。threshold的值="容量*加載因子",當HashMap中                    存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。
  loadFactor就是加載因子。 
  modCount是用來實現fail-fast機制的。

4.HashMap構造函數

/**
     *使用默認的容量及裝載因子構造一個空的HashMap
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//計算下次需要調整大小的極限值
        table = new Entry[DEFAULT_INITIAL_CAPACITY];//根據默認容量(16)初始化table
        init();
    }
/**
     * 根據給定的初始容量的裝載因子創建一個空的HashMap
     * 初始容量小於0或裝載因子小於等於0將報異常 
     */
    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);
        int capacity = 1;
        //設置capacity爲大於initialCapacity且是2的冪的最小值
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
/**
     *根據指定容量創建一個空的HashMap
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);//調用上面的構造方法,容量爲指定的容量,裝載因子是默認值
    }
/**
     *通過傳入的map創建一個HashMap,容量爲默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子爲默認值
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

5.HashMap的內部類Entry<K,V>

HashMap底層是用一個Entry<k,v>數組實現的,每個Entry對象的內部又含有指向下一個Entry類型對象的引用。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//對下一個節點的引用(看到鏈表的內容,結合定義的Entry數組,是不是想到了哈希表的拉鍊法實現?!)
        final int hash;//哈希值

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
        V oldValue = value;
            value = newValue;
            return oldValue;//返回的是之前的Value
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))//先判斷類型是否一致
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
// Key相等且Value相等則兩個Entry相等
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
        // hashCode是Key的hashCode和Value的hashCode的異或的結果
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
        // 重寫toString方法,是輸出更清晰
        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         *當調用put(k,v)方法存入鍵值對時,如果k已經存在,則該方法被調用(爲什麼沒有內容?)
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * 當Entry被從HashMap中移除時被調用(爲什麼沒有內容?)
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }
其中,Map接口:

K getKey();//獲取Key
 V getValue();//獲取Value
V setValue();//設置Value,至於具體返回什麼要看具體實現
 boolean equals(Object o);//定義equals方法用於判斷兩個Entry是否相同
 int hashCode();//定義獲取hashCode的方法

6.HashMap的常用方法解析

6.1  V put(K key, V value)

public V put(K key, V value) {
    // 若“key爲null”,則將該鍵值對添加到table[0]中。
    if (key == null)
        return putForNullKey(value);
    // 若“key不爲null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
put時的步驟爲:①.若key爲null,調用putForNullKey(value);

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
HashMap將“key爲null”的元素都放在table的位置0處。

②.key不爲null

先用hash()得到key的Hash碼,然後通過indexFor得到在數組中的索引。再通過key.equals()在鏈表中找到插入 的位置

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 保存“bucketIndex”位置的值到“e”中
    Entry<K,V> e = table[bucketIndex];
    // 設置“bucketIndex”位置的元素爲“新Entry”,
    // 設置“e”爲“新Entry的下一個節點”
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小
    if (size++ >= threshold)
        resize(2 * table.length);
}
6.2 V get(Object key)
public V get(Object key) {
      if (key == null)
          return getForNullKey();
      // 獲取key的hash值
     int hash = hash(key.hashCode());
     // 在“該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.equals(k)))
             return e.value;
     }
     return null;
 }
6.3 void putAll(Map<? extends K, ? extends V> m)

public void putAll(Map<? extends K, ? extends V> m) {
    // 有效性判斷
    int numKeysToBeAdded = m.size();
    if (numKeysToBeAdded == 0)
        return;

    // 計算容量是否足夠,
    // 若“當前實際容量 < 需要的容量”,則將容量x2。
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
        int newCapacity = table.length;
        while (newCapacity < targetCapacity)
            newCapacity <<= 1;
        if (newCapacity > table.length)
            resize(newCapacity);
    }

    // 通過迭代器,將“m”中的元素逐個添加到HashMap中。
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        put(e.getKey(), e.getValue());
    }
}
6.4 containsKey() 

containsKey() 首先通過getEntry(key)獲取key對應的Entry,然後判斷該Entry是否爲null

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
    // 獲取哈希值
    // HashMap將“key爲null”的元素存儲在table[0]位置,“key不爲null”的則調用hash()計算哈希值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    // 在“該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;
}

7.關於Hash衝突

8.HashMap的優化

容量調整
對於容量的調整,這個是HashMap較爲重點的部分,仔細想想看,對於hashMap我們應該做的是儘量的避免hash衝突 ,此時對於數組的擴容就應該考慮了。不過一個蛋疼的問題也就 出現了,由於新數組的容量變了,原數組的數據就必須重新計算其再數組中的位置,並放入這就是resize。同時這也是最消耗性能的地方。那麼在什麼情況下對HashMap進行擴容呢?一般當HashMap的元素個事超過數組大小**loadFactory的時候,就會進行擴容,而loadFactor就是上文所說的負加載因子。默認值爲0.75 例如數組空間爲16,當元素超過16*0.75=12的時候就把數組大小擴爲2*16=32,然後resize這是一個非常消耗性能的是,因此如果我們預料到HashMap中元素的個數,這就能夠有效的提高hashMap的性能。
負載因子
爲確定何時調整大小,而不是對每個存儲桶中的鏈接列表的深度進行計數,基於hash的  Map使用一個額外的參數並粗略計算存儲桶的密度。Map在調整大小之前,使用名爲LoadFactory的參數指示Map將承擔的“負載”量,即它的負載程度。loadFactory、map大小、容量之間關係: 如果(負載因子)x(容量)>(Map 大小),則調整 Map 大小

數組長度爲2的n次方      

當length總是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。

 假設數組長度分別爲15和16,優化後的hash碼分別爲8和9,那麼&運算後的結果如下:

       h & (table.length-1)                       hash                             table.length-1

       8 & (15-1):                                 0100                   &              1110                   =                0100

       9 & (15-1):                                 0101                   &              1110                   =                0100

       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1):                                 0100                   &              1111                   =                0100

       9 & (16-1):                                 0101                   &              1111                   =                0101

  

  從上面的例子中可以看出:當它們和15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就

產生了碰撞,8和9會被放到數組中的同一個位置上形成鏈表,那麼查詢的時候就需要遍歷這個鏈表,得到8或者9,這樣就降低了查詢的效率。

  同時,我們也可以發現,當數組長度爲15的時候,hash值會與15-1(1110)進行“與”,那麼 最後一位永遠是0,而0001,0011,0101,

1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,

這意味着進一步增加了碰撞的機率,減慢了查詢的效率!

  而當數組長度爲16時,即爲2的n次方時,2n-1得到的二進制數的每個位上的值都爲1,這使得在低位上&時,得到的和原hash的低位相同,

加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值纔會被放到數組中的同一個位置上

形成鏈表。

   所以說,當數組長度爲2的n次冪的時候,不同的key算得得index相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的機率小,

相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

9.總結
a.HashMap 非線程安全
b.初始長度爲16,
c.允許鍵和值爲null


ps:參考以下網友,感謝感謝~~

http://www.cnblogs.com/yuyutianxia/p/3800768.html

http://blog.csdn.net/lcore/article/details/8885961

http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html

 

發佈了82 篇原創文章 · 獲贊 76 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章