【源碼】HashMap源碼剖析,hashmap源碼剖析

來源:互聯網
上載者:User

【源碼】HashMap源碼剖析,hashmap源碼剖析

//-----------------------------------------------------------------------------------

轉載需註明出處:http://blog.csdn.net/chdjj

//-----------------------------------------------------------------------------------

註:以下源碼基於jdk1.7.0_11



之前的幾篇文章介紹了List集合中一些比較常見的類,比如ArrayList、LinkedList、Vector等。本篇文章將介紹集合架構的另一塊內容——Map集合。本文主要介紹HashMap。先回顧下雜湊表。雜湊表定義:根據設定的hash函數和處理衝突的方式(開放定址、公用溢出區、鏈地址、重雜湊...)將一組關鍵字映射到一個有限的連續的地址集上(即bucket數組或桶數組),並以關鍵字在地址集中的“像”作為記錄在表中的儲存位置,這種表稱為hash表,這一映射過程稱為散列,所得儲存位置稱為雜湊地址或散列地址。hash表具有良好的尋找效能,在衝突機率很小的情況下,時間複雜度為O(1)。裝載因子:loadfactor = 表中填入的記錄數/雜湊表的長度.所以loadfactor標誌著雜湊表的裝滿程度。直觀的看,裝載因子越小,發生衝突的機率越小(因為桶中還沒裝幾個資料,就需要擴容),也就是尋找效能越好,但同時浪費的空間就變大。相反,裝載因子越大,發生衝突的機率越大(等到桶快填滿時才能擴容,比如,採用鏈表法處理衝突,在此種情況下,會導致鏈表過長),尋找效能越差,同時浪費的空間會減少。後面我們會看到,HashMap預設的裝載因子為0.75.
下面,依然是自頂向下分析,首先看Map介面:
public interface Map<K,V> {//Map集合的頂級介面定義    // Query Operations    int size();    boolean isEmpty();    boolean containsKey(Object key);//是否包含指定鍵    boolean containsValue(Object value);//是否包含指定值    V get(Object key);    // Modification Operations    V put(K key, V value);    V remove(Object key);    // Bulk Operations    void putAll(Map<? extends K, ? extends V> m);//批量放置元素    void clear();    // Views   //三種視圖    Set<K> keySet();//擷取鍵集    Collection<V> values();//擷取值集    Set<Map.Entry<K, V>> entrySet();//擷取索引值集合    interface Entry<K,V> {//Map的內部介面,代表一個索引值對        K getKey();//擷取鍵        V getValue(); //擷取值        V setValue(V value);//設定值        boolean equals(Object o);        int hashCode();    }    // Comparison and hashing    boolean equals(Object o);    int hashCode();}

Map介面定義了Map集合的操作規範,具體實現交由實作類別完成,其內部有個介面Entry,代表一個鍵值對.
AbstractMap是一個抽象類別,其實現了Map介面中的大部分函數。
public abstract class AbstractMap<K,V> implements Map<K,V>

下面來看幾個方法:

public boolean containsKey(Object key) {        Iterator<Map.Entry<K,V>> i = entrySet().iterator();//獲得迭代器        if (key==null) {//判斷key是否為空白,分別處理            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (e.getKey()==null)//為空白的話使用等號判斷                    return true;            }        } else {            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (key.equals(e.getKey()))//不為空白的話使用equals方法判斷                    return true;            }        }        return false;    }    public boolean containsValue(Object value) {        Iterator<Entry<K,V>> i = entrySet().iterator();        if (value==null) {            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (e.getValue()==null)                    return true;            }        } else {            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (value.equals(e.getValue()))                    return true;            }        }        return false;    }
首先是containsKey和containsValue方法,需要注意的是這裡的key和value是允許為空白的,也就是說其子類預設是支援null鍵和值的。這裡的entrySet方法是個抽象方法:
public abstract Set<Entry<K,V>> entrySet();

abstractMap並沒有實現put方法,而簡單的拋出了異常,這要求子類必須複寫此方法:

 public V put(K key, V value) {        throw new UnsupportedOperationException();    }

但是其卻實現了get方法:
 public V get(Object key) {        Iterator<Entry<K,V>> i = entrySet().iterator();        if (key==null) {//依然是根據索引值是否為null做不同處理            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (e.getKey()==null)                    return e.getValue();            }        } else {            while (i.hasNext()) {                Entry<K,V> e = i.next();                if (key.equals(e.getKey()))                    return e.getValue();            }        }        return null;    }

下面來看HashMap。

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

HashMap繼承了AbstractMap類,並實現了Map介面以及Cloneable、Serializable介面。其成員變數如下:
 static final int DEFAULT_INITIAL_CAPACITY = 16;//預設初始容量    static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量為2的30次方    static final float DEFAULT_LOAD_FACTOR = 0.75f;//預設裝載因子為0.75    transient Entry<K,V>[] table;//桶數組,存放索引值對    transient int size;//實際儲存的索引值對個數    int threshold;//HashMap的閾值,用於判斷是否需要調整HashMap的容量(threshold = 容量*載入因子)    final float loadFactor;//裝載因子    transient int modCount;//hashmap被改變的次數    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

這裡我們得到如下資訊:1.HashMap的預設大小為16,即桶數組的預設長度為16;2.HashMap的預設裝載因子是0.75;3.HashMap內部的桶數組儲存的是Entry對象,也就是鍵值對對象。再看構造器:
 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);        // Find a power of 2 >= initialCapacity        //找出“大於initialCapacity”的最小的2的冪        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        //初始化桶數組        table = new Entry[capacity];        useAltHashing = sun.misc.VM.isBooted() &&                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);        init();//一個鉤子函數,預設實現是空    }    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    public HashMap() {//使用預設的初始容量和預設的載入因子構造HashMap        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    }    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);    }    // internal utilities    void init() {    }

需要注意的點:1.構造器支援指定初始容量和裝載因子,為避免數組擴容帶來的效能問題,建議根據需求指定初始容量。裝載因子盡量不要修改,0.75是個比較靠譜的值。2.實際的容量capacity一般情況下是大於我們傳進去的initialCapacity的,因為內部會通過一個迴圈去找到一個比initialCapacity大且為2的整數次冪的一個數作為實際容量。除非傳進去的數恰好為2的整數次方(capacity取2的整數次冪,是為了使不同hash值發生碰撞的機率較小,這樣就能使元素在雜湊表中均勻地散列。)。
通過前面的分析,我們知道了HashMap內部通過Entry數組儲存鍵值對,那麼這個Entry是怎麼實現的呢?接下來我們看下Entry的實現:
 static class Entry<K,V> implements Map.Entry<K,V> {//實現Map.Entry介面        final K key;//鍵,final類型,不可更改/        V value;//值        Entry<K,V> next;//HashMap通過鏈表法解決衝突,這裡的next指向鏈表的下一個元素        int hash;//hash值        /**         * Creates new entry.         */        //構造器需指定鏈表的下一個結點,所有衝突結點放到一個鏈表上        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;        }        //允許設定value        public final V setValue(V newValue) {            V oldValue = value;            value = newValue;            return oldValue;        }        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();            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;        }        public final int hashCode() {//鍵為空白則hash值為0,否則通過通過hashcode計算            return (key==null   ? 0 : key.hashCode()) ^                   (value==null ? 0 : value.hashCode());        }        public final String toString() {            return getKey() + "=" + getValue();        }        /**         * This method is invoked whenever the value in an entry is         * overwritten by an invocation of put(k,v) for a key k that's already         * in the HashMap.         */        void recordAccess(HashMap<K,V> m) {        }        /**         * This method is invoked whenever the entry is         * removed from the table.         */        void recordRemoval(HashMap<K,V> m) {        }    }

需要注意的點:1.HashMap內部數組儲存的是鍵值對,也就是Entry對象;2.Entry對象儲存了鍵、值,並持有一個next指標指向下一個Entry對象(HashMap通過鏈表法解決衝突);3.Entry可以通過setValue設定值,但不允許設定鍵.
下面我們研究下HashMap中比較重要的方法。從put開始:
  public V put(K key, V value) {//向集合中添加一個索引值對        if (key == null)//如果鍵為空白,則調用putForNullKey            return putForNullKey(value);        int hash = hash(key);//否則根據key產生一個hash索引值        int i = indexFor(hash, table.length);//在根據索引值找到插入位置      //迴圈遍曆指定位置的Entry鏈表,若找到一個鍵與當前鍵完全一致的Entry,那麼覆蓋原來的鍵所對應的值,並返回原值        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            //hash值相同且鍵相同            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;    }

put方法是向hashMap中添加一個鍵值對,這個方法需要注意的有:1.允許鍵為null。put方法會針對null鍵做相應的處理,調用pullforNullKey方法:
  private V putForNullKey(V value) {     //空鍵,其hash值為0,必然儲存在數組的0索引位置上。//我們需要遍曆0位置的entry鏈表,如果已經有一個null鍵了,那麼也是覆蓋        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;    }

2.不允許兩個鍵相同,如果鍵相同,那麼後插入的鍵所對應的值會覆蓋之前的值。3.HashMap是通過調用hash()方法獲得鍵的hash值,並通過indexFor方法找到實際插入位置,具體代碼如下:
  final int hash(Object k) {//根據鍵產生hash值        int h = 0;        if (useAltHashing) {            if (k instanceof String) {                return sun.misc.Hashing.stringHash32((String) k);            }            h = hashSeed;        }        h ^= k.hashCode();        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }//根據hash值計算鍵在桶數組的位置    static int indexFor(int h, int length) {        return h & (length-1);//由put方法可知,這個length就是數組長度,而且由構造器發現數組長度始終為2的整數次方,那麼這個&操作實際上就是是h%length的高效表示方式,可以使結果小於數組長度.    }

4.put方法通過addEntry方法將鍵值插到合適位置:5.當容量超過閾值(threshold)時,會發生擴容,擴容後的數組是原數組的兩倍。
void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {//容量超過閾值            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) {        Entry<K,V> e = table[bucketIndex];//擷取原來在該位置上的Entry對象        table[bucketIndex] = new Entry<>(hash, key, value, e);//將當前的索引值插到該位置,並作為鏈表的起始結點。其next指標指向先前的Entry        size++;    }

這個resize方法就是擴容方法:
void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry[] newTable = new Entry[newCapacity];//建立新數組        boolean oldAltHashing = useAltHashing;        useAltHashing |= sun.misc.VM.isBooted() &&                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);        boolean rehash = oldAltHashing ^ useAltHashing;        transfer(newTable, rehash);//將原數組中所有索引值對轉移至新數組        table = newTable;        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);    }void transfer(Entry[] newTable, boolean rehash) {        int newCapacity = newTable.length;        for (Entry<K,V> e : table) {//需遍曆每個Entry,耗時            while(null != e) {                Entry<K,V> next = e.next;                if (rehash) {                    e.hash = null == e.key ? 0 : hash(e.key);                }                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            }        }    }

6.擴容操作需要開闢新數組,並對原數組中所有鍵值對重新散列,非常耗時。我們應該盡量避免HashMap擴容。
再來看get方法:
public V get(Object key) {        if (key == null)//若鍵為空白            return getForNullKey();        Entry<K,V> entry = getEntry(key);//擷取Entry對象//未找到就返回null,否則返回鍵所對應的值        return null == entry ? null : entry.getValue();    }

這個getForNullKey方法就是在數組0索引位上的鏈表去尋找null鍵:
 private V getForNullKey() {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null)                return e.value;        }        return null;    }

而這個getEntry方法就是通過鍵產生hash值,然後得到其在數組的索引位,尋找該位置的鏈表,找到第一個滿足的鍵,並返會Entry對象:

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

再看下remove方法:
public V remove(Object key) {        Entry<K,V> e = removeEntryForKey(key);        return (e == null ? null : e.value);    }    final Entry<K,V> removeEntryForKey(Object key) {        int hash = (key == null) ? 0 : hash(key);//先計算hash值        int i = indexFor(hash, table.length);//找到索引位        Entry<K,V> prev = table[i];        Entry<K,V> e = prev;        while (e != null) {//遍曆鏈表找到該鍵,並修改鏈表相關指標指向            Entry<K,V> next = e.next;            Object k;            if (e.hash == hash &&                ((k = e.key) == key || (key != null && key.equals(k)))) {                modCount++;                size--;                if (prev == e)                    table[i] = next;                else                    prev.next = next;                e.recordRemoval(this);                return e;            }            prev = e;            e = next;        }        return e;    }

最後看下clear方法:
 public void clear() {        modCount++;        Entry[] tab = table;        for (int i = 0; i < tab.length; i++)//遍曆數組            tab[i] = null;//置空        size = 0;    }

總結:1.HashMap的預設大小為16,即桶數組的預設長度為16;2.HashMap的預設裝載因子是0.75;3.HashMap內部的桶數組儲存的是Entry對象,也就是鍵值對對象。
4.構造器支援指定初始容量和裝載因子,為避免數組擴容帶來的效能問題,建議根據需求指定初始容量。裝載因子盡量不要修改,0.75是個比較靠譜的值。5.桶數組的長度始終是2的整數次方(大於等於指定的初始容量),這樣做可以減少衝突機率,提高尋找效率。(可以從indexfor函數中看出,h&(length-1),若length為奇數,length-1為偶數那麼h&(length-1)結果的最後一位必然為0,也就是說所有鍵都被散列到數組的偶數下標位置,這樣會浪費近一半空間。另外,length為2的整數次方也保證了h&(length-1)與h%length等效).6.HashMap接受null鍵;7.HashMap不允許鍵重複,但是值是可以重複的。若鍵重複,那麼新值會覆蓋舊值。8.HashMap通過鏈表法解決衝突問題,每個Entry都有一個next指標指向下一個Entry,衝突元素(不是鍵相同,而是hash值相同)會構成一個鏈表。並且最新插入的鍵值對始終位於鏈表首部。9.當容量超過閾值(threshold)時,會發生擴容,擴容後的數組是原數組的兩倍。擴容操作需要開闢新數組,並對原數組中所有鍵值對重新散列,非常耗時。我們應該盡量避免HashMap擴容。10.HashMap非安全執行緒。



HashMap源碼中put方法裡面ehash == hash && ((k = ekey) == key || keyequals(k))

如果兩個對象相同,那麼它們的hashCode值一定要相同,所以先e.hash == hash,但是這個只是必要條件,而不是充分條件,也就是說,如果兩個對象的hashCode相同,它們並不一定是同一個對象,所以後面還要key.equals(k)
 
java查看hashmap的源碼發現並沒有向entrySet中裝入元素,而去可以如下遍曆

協助文檔上說:返回此映射所包含的映射關係的 collection 視圖。在返回的集合中,每個元素都是一個 Map.Entry。
entrySet僅僅是一個視圖而已,沒有具體的資料,其實還是從HashMap中擷取資料的。具體可以看entry和entrySet的原始碼就知道資料其實還是來自於table。
 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.