在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Java/ HashMap
Java 集合細(xì)節(jié)(四):保持 compareTo 和 equals 同步
Iterator
使用序列化實(shí)現(xiàn)對(duì)象的拷貝
fail-fast 機(jī)制
關(guān)鍵字 final
Vector
HashTable
Java 集合細(xì)節(jié)(一):請(qǐng)為集合指定初始容量
強(qiáng)制類型轉(zhuǎn)換
數(shù)組之一:認(rèn)識(shí) JAVA 數(shù)組
Java 集合細(xì)節(jié)(三):subList 的缺陷
hashCode
ArrayList
數(shù)組之二
List 總結(jié)
LinkedList
Java 提高篇(九)—–實(shí)現(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 定時(shí)任務(wù)
HashSet
字符串
理解 Java 的三大特性之繼承
理解 Java 的三大特性之封裝
代碼塊

HashMap

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

一、定義

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


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

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

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

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

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

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

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

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)都可以利用這兩種來(lái)組合實(shí)現(xiàn),HashMap 也是如此。實(shí)際上 HashMap 是一個(gè)“鏈表散列”,如下是它數(shù)據(jù)結(jié)構(gòu):

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

從上圖我們可以看出 HashMap 底層實(shí)現(xiàn)還是數(shù)組,只是數(shù)組的每一項(xiàng)都是一條鏈。其中參數(shù) initialCapacity 就代表了該數(shù)組的長(zhǎng)度。下面為 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);

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

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

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


    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、下一個(gè)節(jié)點(diǎn) next,以及 hash 值,這是非常重要的,正是由于 Entry 才構(gòu)成了 table 數(shù)組的項(xiàng)為鏈表。

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

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

首先我們先看源碼


    public V put(K key, V value) {
            //當(dāng)key為null,調(diào)用putForNullKey方法,保存null與table第一個(gè)位置中,這是HashMap允許為null的原因
            if (key == null)
                return putForNullKey(value);
            //計(jì)算key的hash值
            int hash = hash(key.hashCode());                   ------(1)
            //計(jì)算key hash 值在 table 數(shù)組中的位置
            int i = indexFor(hash, table.length);             ------(2)
            //從i出開(kā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;
        }

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

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

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


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

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


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

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

我們回到 indexFor 方法,該方法僅有一條語(yǔ)句:h&(length – 1),這句話除了上面的取模運(yùn)算外還有一個(gè)非常重要的責(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 時(shí),6 和 7 的結(jié)果一樣,這樣表示他們?cè)?table 存儲(chǔ)的位置是相同的,也就是產(chǎn)生了碰撞,6、7 就會(huì)在一個(gè)位置形成鏈表,這樣就會(huì)導(dǎo)致查詢速度降低。誠(chéng)然這里只分析三個(gè)數(shù)字不是很多,那么我們就看 0-15。

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

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

這里我們?cè)賮?lái)復(fù)習(xí) put 的流程:當(dāng)我們想一個(gè) HashMap 中添加一對(duì) key-value 時(shí),系統(tǒng)首先會(huì)計(jì)算 key 的 hash 值,然后根據(jù) hash 值確認(rèn)在 table 中存儲(chǔ)的位置。若該位置沒(méi)有元素,則直接插入。否則迭代該處元素鏈表并依此比較其 key 的 hash 值。如果兩個(gè) hash 值相等且 key 值相等 (e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的 Entry 的 value 覆蓋原來(lái)節(jié)點(diǎn)的 value。如果兩個(gè) hash 值相等但 key 值不等 ,則將該節(jié)點(diǎn)插入該鏈表的鏈頭。具體的實(shí)現(xiàn)過(guò)程見(jià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 指向原來(lái)的 Entry 
            table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
            //若HashMap中元素的個(gè)數(shù)超過(guò)極限了,則容量擴(kuò)大兩倍
            if (size++ >= threshold)
                resize(2 * table.length);
        }

這個(gè)方法中有兩點(diǎn)需要注意:

一、鏈的產(chǎn)生

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

二、擴(kuò)容問(wèn)題。

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

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

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


    public V get(Object key) {
            // 若為null,調(diào)用getForNullKey方法返回相對(duì)應(yīng)的value
            if (key == null)
                return getForNullKey();
            // 根據(jù)該 key 的 hashCode 值計(jì)算它的 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相同,則返回相對(duì)應(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)系,在前面就提到過(guò),HashMap 在存儲(chǔ)過(guò)程中并沒(méi)有將 key,value 分開(kāi)來(lái)存儲(chǔ),而是當(dāng)做一個(gè)整體 key-value 來(lái)處理的,這個(gè)整體就是 Entry 對(duì)象。同時(shí) value 也只相當(dāng)于 key 的附屬而已。在存儲(chǔ)的過(guò)程中,系統(tǒng)根據(jù) key 的 hashcode 來(lái)決定 Entry 在 table 數(shù)組中的存儲(chǔ)位置,在取的過(guò)程中同樣根據(jù) key 的 hashcode 取出相對(duì)應(yīng)的 Entry 對(duì)象。

上一篇:LinkedList