1. 程式人生 > >Java集合篇:Map總結

Java集合篇:Map總結

相關閱讀:

Java基礎篇:hashCode的作用

Java集合篇:HashMap原理詳解

Java集合篇:Hashtable原理詳解 、Hashtable與HashMap的區別

Java集合篇:ConcurrentHashMap詳解(JDK1.6)

Java集合篇:ConcurrentHashMap詳解(JDK1.8)

 

一、Map概述:

首先先看Map的結構示意圖:

        Map:“鍵值”對對映的抽象介面。該對映不包括重複的鍵,一個鍵對應一個值。

        SortedMap:有序的鍵值對介面,繼承Map介面。

        NavigableMap:繼承SortedMap,具有了針對給定搜尋目標返回最接近匹配項的導航方法的介面。

        AbstractMap:實現了Map中的絕大部分函式介面。它減少了“Map的實現類”的重複編碼。

        Dictionary:任何可將鍵對映到相應值的類的抽象父類。目前被Map介面取代。

        TreeMap:有序散列表,實現SortedMap 介面,底層通過紅黑樹實現。

        HashMap:是基於“拉鍊法”實現的散列表。底層採用“陣列+連結串列”實現。

        WeakHashMap:基於“拉鍊法”實現的散列表。

        HashTable:基於“拉鍊法”實現的散列表。

總結如下:

他們之間的區別:

 

二、內部雜湊:雜湊對映技術:

幾乎所有通用Map都使用雜湊對映技術。雜湊對映技術是一種將元素對映到陣列的非常簡單的技術。由於雜湊對映採用的是陣列集合,那麼必然存在一中用於確定任意鍵訪問陣列的索引機制,該機制能夠提供一個小於陣列大小的整數,我們將該機制稱之為雜湊函式。在Java中我們不必為尋找這樣的整數而大傷腦筋,因為每個物件都必定存在一個返回整數值的hashCode方法,而我們需要做的就是將其轉換為整數,然後再將該值除以陣列大小取餘即可。如下:

int hashValue = Maths.abs(obj.hashCode()) % size;

下面是HashMap、HashTable的:

----------HashMap------------
//計算hash值
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
//計算key的索引位置
static int indexFor(int h, int length) {
        return h & (length-1);
}
-----HashTable--------------
int index = (hash & 0x7FFFFFFF) % tab.length;     //確認該key的索引位置

位置的索引就代表了該節點在陣列中的位置。下圖是雜湊對映的基本原理圖:

在該圖中1-4步驟是找到該元素在陣列中位置,5-8步驟是將該元素插入陣列中。在插入的過程中會遇到一點點小挫折。在眾多肯能存在多個元素他們的hash值是一樣的,這樣就會得到相同的索引位置,也就說多個元素會對映到相同的位置,這個過程我們稱之為“衝突”。解決衝突的辦法就是在索引位置處插入一個連結列表,並簡單地將元素新增到此連結列表。當然也不是簡單的插入,在HashMap中的處理過程如下:獲取索引位置的連結串列,如果該連結串列為null,則將該元素直接插入,否則通過比較是否存在與該key相同的key,若存在則覆蓋原來key的value並返回舊值,否則將該元素儲存在鏈頭(最先儲存的元素放在鏈尾)。下面是HashMap的put方法,該方法詳細展示了計算索引位置,將元素插入到適當的位置的全部過程:
 

 
public V put(K key, V value) {
        //當key為null,呼叫putForNullKey方法,儲存null與table第一個位置中,這是HashMap允許為null的原因
        if (key == null)
            return putForNullKey(value);
        //計算key的hash值
        int hash = hash(key.hashCode());                 
        //計算key hash 值在 table 陣列中的位置
        int i = indexFor(hash, table.length);            
        //從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;     //返回舊值
            }
        }
        //修改次數增加1
        modCount++;
        //將key、value新增至i位置處
        addEntry(hash, key, value, i);
        return null;
    }
 

HashMap的put方法展示了雜湊對映的基本思想,其實如果我們檢視其它的Map,發現其原理都差不多!

 

三、Map優化:

首先我們這樣假設,假設雜湊對映的內部陣列的大小隻有1,所有的元素都將對映該位置(0),從而構成一條較長的連結串列。由於我們更新、訪問都要對這條連結串列進行線性搜尋,這樣勢必會降低效率。我們假設,如果存在一個非常大陣列,每個位置連結串列處都只有一個元素,在進行訪問時計算其 index 值就會獲得該物件,這樣做雖然會提高我們搜尋的效率,但是它浪費了控制元件。誠然,雖然這兩種方式都是極端的,但是它給我們提供了一種優化思路:使用一個較大的陣列讓元素能夠均勻分佈。在Map有兩個會影響到其效率,一是容器的初始化大小、二是負載因子

3.1、調整實現大小:

在雜湊對映表中,內部陣列中的每個位置稱作“儲存桶”(bucket),而可用的儲存桶數(即內部陣列的大小)稱作容量 (capacity),我們為了使Map物件能夠有效地處理任意數的元素,將Map設計成可以調整自身的大小。我們知道當Map中的元素達到一定量的時候就會調整容器自身的大小,但是這個調整大小的過程其開銷是非常大的。調整大小需要將原來所有的元素插入到新陣列中。我們知道index = hash(key) % length。這樣可能會導致原先衝突的鍵不在衝突,不衝突的鍵現在衝突的,重新計算、調整、插入的過程開銷是非常大的,效率也比較低下。所以,如果我們開始知道Map的預期大小值,將Map調整的足夠大,則可以大大減少甚至不需要重新調整大小,這很有可能會提高速度。下面是HashMap調整容器大小的過程,通過下面的程式碼我們可以看到其擴容過程的複雜性:

void resize(int newCapacity) {
            Entry[] oldTable = table;    //原始容器
            int oldCapacity = oldTable.length;    //原始容器大小
            if (oldCapacity == MAXIMUM_CAPACITY) {     //是否超過最大值:1073741824
                threshold = Integer.MAX_VALUE;
                return;
            }
 
            //新的陣列:大小為 oldCapacity * 2
            Entry[] newTable = new Entry[newCapacity];    
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
           /*
            * 重新計算閥值 =  newCapacity * loadFactor >  MAXIMUM_CAPACITY + 1 ? 
            *                         newCapacity * loadFactor :MAXIMUM_CAPACITY + 1
            */
            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) {
                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;
                }
            }
        }

3.2、負載因子:

為了確認何時需要調整Map容器,Map使用了一個額外的引數並且粗略計算儲存容器的密度。在Map調整大小之前,使用”負載因子”來指示Map將會承擔的“負載量”,也就是它的負載程度,當容器中元素的數量達到了這個“負載量”,則Map將會進行擴容操作。

負載因子、容量、Map大小之間的關係如下:負載因子 * 容量 > map大小  ----->調整Map大小。

例如:如果負載因子大小為0.75(HashMap的預設值),預設容量為16,則 16 * 0.75 = 12,所以當我們容器中插入第12個元素的時候,Map就會調整大小。

負載因子本身就是在控制元件和時間之間的折衷。當我使用較小的負載因子時,雖然降低了衝突的可能性,使得單個連結串列的長度減小了,加快了訪問和更新的速度,但是它佔用了更多的控制元件,使得陣列中的大部分控制元件沒有得到利用,元素分佈比較稀疏,同時由於Map頻繁的調整大小,可能會降低效能。但是如果負載因子過大,會使得元素分佈比較緊湊,導致產生衝突的可能性加大,從而訪問、更新速度較慢。所以我們一般推薦不更改負載因子的值,採用預設值0.75.
 

 

原文轉自:https://blog.csdn.net/chenssy/article/details/37909815