鍍金池/ 教程/ Java/ HashMap
Java 集合細(xì)節(jié)(四):保持 compareTo 和 equals 同步
Iterator
使用序列化實現(xiàn)對象的拷貝
fail-fast 機(jī)制
關(guān)鍵字 final
Vector
HashTable
Java 集合細(xì)節(jié)(一):請為集合指定初始容量
強(qiáng)制類型轉(zhuǎn)換
數(shù)組之一:認(rèn)識 JAVA 數(shù)組
Java 集合細(xì)節(jié)(三):subList 的缺陷
hashCode
ArrayList
數(shù)組之二
List 總結(jié)
LinkedList
Java 提高篇(九)—–實現(xiàn)多重繼承
Java 的四舍五入
關(guān)鍵字 static
理解 Java 的三大特性之多態(tài)
抽象類與接口
集合大家族
異常(二)
Java 集合細(xì)節(jié)(二):asList 的缺陷
Map 總結(jié)
TreeSet
equals() 方法總結(jié)
Java 提高篇(十)—–詳解匿名內(nèi)部類
HashMap
Stack
詳解內(nèi)部類
TreeMap
異常(一)
詳解 Java 定時任務(wù)
HashSet
字符串
理解 Java 的三大特性之繼承
理解 Java 的三大特性之封裝
代碼塊

HashMap

HashMap 也是我們使用非常多的 Collection,它是基于哈希表的 Map 接口的實現(xiàn),以 key-value 的形式存在。在 HashMap 中,key-value 總是會當(dāng)做一個整體來處理,系統(tǒng)會根據(jù) hash 算法來來計算 key-value 的存儲位置,我們總是可以通過 key 快速地存、取 value。下面就來分析 HashMap 的存取。

一、定義

HashMap 實現(xiàn)了 Map 接口,繼承 AbstractMap。其中 Map 接口定義了鍵映射到值的規(guī)則,而 AbstractMap 類提供 Map 接口的骨干實現(xiàn),以最大限度地減少實現(xiàn)此接口所需的工作,其實 AbstractMap 類已經(jīng)實現(xiàn)了Map,這里標(biāo)注 Map LZ 覺得應(yīng)該是更加清晰吧!


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

二、構(gòu)造函數(shù)

HashMap 提供了三個構(gòu)造函數(shù):

HashMap():構(gòu)造一個具有默認(rèn)初始容量 (16) 和默認(rèn)加載因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity):構(gòu)造一個帶指定初始容量和默認(rèn)加載因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity, float loadFactor):構(gòu)造一個帶指定初始容量和加載因子的空 HashMap。

在這里提到了兩個參數(shù):初始容量,加載因子。這兩個參數(shù)是影響 HashMap 性能的重要參數(shù),其中容量表示哈希表中桶的數(shù)量,初始容量是創(chuàng)建哈希表時的容量,加載因子是哈希表在其容量自動增加之前可以達(dá)到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負(fù)載因子越大表示散列表的裝填程度越高,反之愈小。對于使用鏈表法的散列表來說,查找一個元素的平均時間是 O(1+a),因此如果負(fù)載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負(fù)載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴(yán)重浪費。系統(tǒng)默認(rèn)負(fù)載因子為 0.75,一般情況下我們是無需修改的。

HashMap 是一種支持快速存取的數(shù)據(jù)結(jié)構(gòu),要了解它的性能必須要了解它的數(shù)據(jù)結(jié)構(gòu)。

三、數(shù)據(jù)結(jié)構(gòu)

我們知道在 Java 中最常用的兩種結(jié)構(gòu)是數(shù)組和模擬指針(引用),幾乎所有的數(shù)據(jù)結(jié)構(gòu)都可以利用這兩種來組合實現(xiàn),HashMap 也是如此。實際上 HashMap 是一個“鏈表散列”,如下是它數(shù)據(jù)結(jié)構(gòu):

http://wiki.jikexueyuan.com/project/java-enhancement/images/23-1.png" alt="fig.1" />

從上圖我們可以看出 HashMap 底層實現(xiàn)還是數(shù)組,只是數(shù)組的每一項都是一條鏈。其中參數(shù) initialCapacity 就代表了該數(shù)組的長度。下面為 HashMap 構(gòu)造函數(shù)的源碼:


    public HashMap(int initialCapacity, float loadFactor) {
            //初始容量不能<0
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: "
                        + initialCapacity);
            //初始容量不能 > 最大容量值,HashMap的最大容量值為2^30
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            //負(fù)載因子不能 < 0
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: "
                        + loadFactor);

            // 計算出大于 initialCapacity 的最小的 2 的 n 次方值。
            int capacity = 1;
            while (capacity < initialCapacity)
                capacity <<= 1;

            this.loadFactor = loadFactor;
            //設(shè)置HashMap的容量極限,當(dāng)HashMap的容量達(dá)到該極限時就會進(jìn)行擴(kuò)容操作
            threshold = (int) (capacity * loadFactor);
            //初始化table數(shù)組
            table = new Entry[capacity];
            init();
        }

從源碼中可以看出,每次新建一個 HashMap 時,都會初始化一個 table 數(shù)組。table 數(shù)組的元素為 Entry 節(jié)點。


    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            final int hash;

            /**
             * Creates new entry.
             */
            Entry(int h, K k, V v, Entry<K,V> n) {
                value = v;
                next = n;
                key = k;
                hash = h;
            }
            .......
        }

其中 Entry 為 HashMap 的內(nèi)部類,它包含了鍵 key、值 value、下一個節(jié)點 next,以及 hash 值,這是非常重要的,正是由于 Entry 才構(gòu)成了 table 數(shù)組的項為鏈表。

上面簡單分析了 HashMap 的數(shù)據(jù)結(jié)構(gòu),下面將探討 HashMap 是如何實現(xiàn)快速存取的。

四、存儲實現(xiàn):put(key,vlaue)

首先我們先看源碼


    public V put(K key, V value) {
            //當(dāng)key為null,調(diào)用putForNullKey方法,保存null與table第一個位置中,這是HashMap允許為null的原因
            if (key == null)
                return putForNullKey(value);
            //計算key的hash值
            int hash = hash(key.hashCode());                   ------(1)
            //計算key hash 值在 table 數(shù)組中的位置
            int i = indexFor(hash, table.length);             ------(2)
            //從i出開始迭代 e,找到 key 保存的位置
            for (Entry<K, V> e = table[i]; e != null; e = e.next) {
                Object k;
                //判斷該條鏈上是否有hash值相同的(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;     //返回舊值
                }
            }
            //修改次數(shù)增加1
            modCount++;
            //將key、value添加至i位置處
            addEntry(hash, key, value, i);
            return null;
        }

通過源碼我們可以清晰看到 HashMap 保存數(shù)據(jù)的過程為:首先判斷 key 是否為 null,若為 null,則直接調(diào)用 putForNullKey 方法。若不為空則先計算 key 的 hash 值,然后根據(jù) hash 值搜索在 table 數(shù)組中的索引位置,如果 table 數(shù)組在該位置處有元素,則通過比較是否存在相同的 key,若存在則覆蓋原來 key 的 value,否則將該元素保存在鏈頭(最先保存的元素放在鏈尾)。若 table 在該處沒有元素,則直接保存。這個過程看似比較簡單,其實深有內(nèi)幕。有如下幾點:

1、 先看迭代處。此處迭代原因就是為了防止存在相同的 key 值,若發(fā)現(xiàn)兩個 hash 值(key)相同時,HashMap 的處理方式是用新 value 替換舊 value,這里并沒有處理 key,這就解釋了 HashMap 中沒有兩個相同的 key。

2、 在看(1)、(2)處。這里是 HashMap 的精華所在。首先是 hash 方法,該方法為一個純粹的數(shù)學(xué)計算,就是計算 h 的 hash 值。


    static int hash(int h) {
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

我們知道對于 HashMap 的 table 而言,數(shù)據(jù)分布需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太松,太緊會導(dǎo)致查詢速度慢,太松則浪費空間。計算 hash 值后,怎么才能保證 table 元素分布均與呢?我們會想到取模,但是由于取模的消耗較大,HashMap 是這樣處理的:調(diào)用 indexFor 方法。


    static int indexFor(int h, int length) {
            return h & (length-1);
        }

HashMap 的底層數(shù)組長度總是 2 的 n 次方,在構(gòu)造函數(shù)中存在:capacity <<= 1;這樣做總是能夠保證 HashMap 的底層數(shù)組長度為 2 的 n 次方。當(dāng) length 為 2 的 n 次方時,h&(length – 1) 就相當(dāng)于對 length 取模,而且速度比直接取??斓枚啵@是 HashMap 在速度上的一個優(yōu)化。至于為什么是 2 的 n 次方下面解釋。

我們回到 indexFor 方法,該方法僅有一條語句:h&(length – 1),這句話除了上面的取模運算外還有一個非常重要的責(zé)任:均勻分布 table 數(shù)據(jù)和充分利用空間。

這里我們假設(shè) length 為 16(2^n) 和 15,h 為 5、6、7。

http://wiki.jikexueyuan.com/project/java-enhancement/images/23-2.jpg" alt="fig.2" />

當(dāng) n=15 時,6 和 7 的結(jié)果一樣,這樣表示他們在 table 存儲的位置是相同的,也就是產(chǎn)生了碰撞,6、7 就會在一個位置形成鏈表,這樣就會導(dǎo)致查詢速度降低。誠然這里只分析三個數(shù)字不是很多,那么我們就看 0-15。

http://wiki.jikexueyuan.com/project/java-enhancement/images/23-3.jpg" alt="fig.3" />

從上面的圖表中我們看到總共發(fā)生了 8 此碰撞,同時發(fā)現(xiàn)浪費的空間非常大,有 1、3、5、7、9、11、13、15 處沒有記錄,也就是沒有存放數(shù)據(jù)。這是因為他們在與 14 進(jìn)行 & 運算時,得到的結(jié)果最后一位永遠(yuǎn)都是 0,即 0001、0011、0101、0111、1001、1011、1101、1111 位置處是不可能存儲數(shù)據(jù)的,空間減少,進(jìn)一步增加碰撞幾率,這樣就會導(dǎo)致查詢速度慢。而當(dāng) length = 16 時,length – 1 = 15 即 1111,那么進(jìn)行低位 & 運算時,值總是與原來 hash 值相同,而進(jìn)行高位運算時,其值等于其低位值。所以說當(dāng) length = 2^n 時,不同的 hash 值發(fā)生碰撞的概率比較小,這樣就會使得數(shù)據(jù)在 table 數(shù)組中分布較均勻,查詢速度也較快。

這里我們再來復(fù)習(xí) put 的流程:當(dāng)我們想一個 HashMap 中添加一對 key-value 時,系統(tǒng)首先會計算 key 的 hash 值,然后根據(jù) hash 值確認(rèn)在 table 中存儲的位置。若該位置沒有元素,則直接插入。否則迭代該處元素鏈表并依此比較其 key 的 hash 值。如果兩個 hash 值相等且 key 值相等 (e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的 Entry 的 value 覆蓋原來節(jié)點的 value。如果兩個 hash 值相等但 key 值不等 ,則將該節(jié)點插入該鏈表的鏈頭。具體的實現(xiàn)過程見 addEntry 方法,如下:


    void addEntry(int hash, K key, V value, int bucketIndex) {
            //獲取bucketIndex處的Entry
            Entry<K, V> e = table[bucketIndex];
            //將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來的 Entry 
            table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
            //若HashMap中元素的個數(shù)超過極限了,則容量擴(kuò)大兩倍
            if (size++ >= threshold)
                resize(2 * table.length);
        }

這個方法中有兩點需要注意:

一、鏈的產(chǎn)生

這是一個非常優(yōu)雅的設(shè)計。系統(tǒng)總是將新的 Entry 對象添加到 bucketIndex 處。如果 bucketIndex 處已經(jīng)有了對象,那么新添加的 Entry 對象將指向原有的 Entry 對象,形成一條 Entry 鏈,但是若 bucketIndex 處沒有 Entry 對象,也就是 e==null,那么新添加的 Entry 對象指向 null,也就不會產(chǎn)生 Entry 鏈了。

二、擴(kuò)容問題。

隨著 HashMap 中元素的數(shù)量越來越多,發(fā)生碰撞的概率就越來越大,所產(chǎn)生的鏈表長度就會越來越長,這樣勢必會影響 HashMap 的速度,為了保證 HashMap 的效率,系統(tǒng)必須要在某個臨界點進(jìn)行擴(kuò)容處理。該臨界點在當(dāng) HashMap 中元素的數(shù)量等于 table 數(shù)組長度 * 加載因子。但是擴(kuò)容是一個非常耗時的過程,因為它需要重新計算這些數(shù)據(jù)在新 table 數(shù)組中的位置并進(jìn)行復(fù)制處理。所以如果我們已經(jīng)預(yù)知 HashMap 中元素的個數(shù),那么預(yù)設(shè)元素的個數(shù)能夠有效的提高 HashMap 的性能。

五、讀取實現(xiàn):get(key)

相對于 HashMap 的存而言,取就顯得比較簡單了。通過 key 的 hash 值找到在 table 數(shù)組中的索引處的 Entry,然后返回該 key 對應(yīng)的 value 即可。


    public V get(Object key) {
            // 若為null,調(diào)用getForNullKey方法返回相對應(yīng)的value
            if (key == null)
                return getForNullKey();
            // 根據(jù)該 key 的 hashCode 值計算它的 hash 碼  
            int hash = hash(key.hashCode());
            // 取出 table 數(shù)組中指定索引處的值
            for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
                Object k;
                //若搜索的key與查找的key相同,則返回相對應(yīng)的value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e.value;
            }
            return null;
        }

在這里能夠根據(jù) key 快速的取到 value 除了和 HashMap 的數(shù)據(jù)結(jié)構(gòu)密不可分外,還和 Entry 有莫大的關(guān)系,在前面就提到過,HashMap 在存儲過程中并沒有將 key,value 分開來存儲,而是當(dāng)做一個整體 key-value 來處理的,這個整體就是 Entry 對象。同時 value 也只相當(dāng)于 key 的附屬而已。在存儲的過程中,系統(tǒng)根據(jù) key 的 hashcode 來決定 Entry 在 table 數(shù)組中的存儲位置,在取的過程中同樣根據(jù) key 的 hashcode 取出相對應(yīng)的 Entry 對象。

上一篇:LinkedList