Java提升篇(三三)-----Map總結

在前面LZ詳細介紹了HashMapHashTableTreeMap的實現方法,從數據結構、實現原理、源碼分析三個方面進行闡述,對這個三個類應該有了比較清晰的瞭解,下面LZ就Map作一個簡單的總結。html

推薦閱讀:java

java提升篇(二三)—–HashMap程序員

java提升篇(二五)—–HashTable數組

Java提升篇(二六)-----hashCode數據結構

Java提升篇(二七)—–TreeMap函數

1、Map概述

首先先看Map的結構示意圖源碼分析

2014071500001

Map:「鍵值」對映射的抽象接口。該映射不包括重複的鍵,一個鍵對應一個值。性能

SortedMap:有序的鍵值對接口,繼承Map接口。優化

NavigableMap:繼承SortedMap,具備了針對給定搜索目標返回最接近匹配項的導航方法的接口。this

AbstractMap:實現了Map中的絕大部分函數接口。它減小了「Map的實現類」的重複編碼。

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

TreeMap:有序散列表,實現SortedMap 接口,底層經過紅黑樹實現。

HashMap:是基於「拉鍊法」實現的散列表。底層採用「數組+鏈表」實現。

WeakHashMap:基於「拉鍊法」實現的散列表。

HashTable:基於「拉鍊法」實現的散列表。

總結以下:

2014071500002

他們之間的區別:

2014071500003

2、內部哈希: 哈希映射技術

幾乎全部通用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的索引位置
 位置的索引就表明了該節點在數組中的位置。下圖是哈希映射的基本原理圖:
2014071500004
在該圖中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,發現其原理都差很少!

3、Map優化

首先咱們這樣假設,假設哈希映射的內部數組的大小隻有1,全部的元素都將映射該位置(0),從而構成一條較長的鏈表。因爲咱們更新、訪問都要對這條鏈表進行線性搜索,這樣勢必會下降效率。咱們假設,若是存在一個很是大數組,每一個位置鏈表處都只有一個元素,在進行訪問時計算其 index 值就會得到該對象,這樣作雖然會提升咱們搜索的效率,可是它浪費了控件。誠然,雖然這兩種方式都是極端的,可是它給咱們提供了一種優化思路:使用一個較大的數組讓元素可以均勻分佈。在Map有兩個會影響到其效率,一是容器的初始化大小、二是負載因子。

3.一、調整實現大小

在哈希映射表中,內部數組中的每一個位置稱做「存儲桶」(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.二、負載因子

爲了確認什麼時候須要調整Map容器,Map使用了一個額外的參數而且粗略計算存儲容器的密度。在Map調整大小以前,使用」負載因子」來指示Map將會承擔的「負載量」,也就是它的負載程度,當容器中元素的數量達到了這個「負載量」,則Map將會進行擴容操做。負載因子、容量、Map大小之間的關係以下:負載因子 * 容量 > map大小  ----->調整Map大小。

例如:若是負載因子大小爲0.75(HashMap的默認值),默認容量爲11,則 11 * 0.75 = 8.25 = 8,因此當咱們容器中插入第八個元素的時候,Map就會調整大小。

負載因子自己就是在控件和時間之間的折衷。當我使用較小的負載因子時,雖然下降了衝突的可能性,使得單個鏈表的長度減少了,加快了訪問和更新的速度,可是它佔用了更多的控件,使得數組中的大部分控件沒有獲得利用,元素分佈比較稀疏,同時因爲Map頻繁的調整大小,可能會下降性能。可是若是負載因子過大,會使得元素分佈比較緊湊,致使產生衝突的可能性加大,從而訪問、更新速度較慢。因此咱們通常推薦不更改負載因子的值,採用默認值0.75.

最後

推薦閱讀:

java提升篇(二三)—–HashMap

java提升篇(二五)—–HashTable

Java提升篇(二六)-----hashCode

Java提升篇(二七)—–TreeMap


-----原文出自:http://cmsblogs.com/?p=1212,請尊重做者辛勤勞動成果,轉載說明出處.

-----我的站點:http://cmsblogs.com

相關文章
相關標籤/搜索