1. 程式人生 > >Java集合源碼分析--HashMap

Java集合源碼分析--HashMap

nal 容量 多個 shm ali AC java 註意 .com

轉載自 http://www.cnblogs.com/zhangyinhua/p/7698642.html#_label0

一,關於HashMap API定義

1、哈希表基於map接口的實現,這個實現提供了map所有的操作,並且提供了key和value可以為null,(HashMap和HashTable大致上是一樣的除了hashmap是異步的和允許key和value為null),
這個類不確定map中元素的位置,特別要提的是,這個類也不確定元素的位置隨著時間會不會保持不變。
2.
HashMap的實例有兩個參數影響性能,初始化容量(initialCapacity)和loadFactor加載因子。

二,HashMap 的屬性


HashMap的實例有兩個參數影響其性能。

  初始容量:哈希表中桶的數量  加載因子:哈希表在其容量自動增加之前可以達到多滿的一種尺度

  當哈希表中條目數超出了當前容量*加載因子(其實就是HashMap的實際容量)時,則對該哈希表進行rehash操作,將哈希表擴充至兩倍的桶數。

//Java中默認初始容量為16,加載因子為0.75。        
HashMap hm=new HashMap(); for(int i=0;i<17;i++){ hm.put(i,i); } System.out.println(hm.size());
//17 System.out.println(hm.table.length);//32

1)loadFactor加載因子

    定義:loadFactor譯為裝載因子。裝載因子用來衡量HashMap滿的程度,是控制數組存放數據的疏密程度。計算HashMap的實時裝載因子的方法為:size/capacity,而不是占用桶的數量去除以capacity。

    loadFactor越趨近於1,那麽數組中存放的數據(entry)也就越多,但我們在通過key拿到我們的value時,是先通過key的hashcode值,找到對應數組中的位置,

如果該位置中有很多元素,則需要通過equals來依次比較鏈表中的元素,拿到我們的value值,這樣花費的性能就很高,

    loadFactor越趨近於0那麽數組中存放的數據也就越稀,分散的太開,浪費很多空間,這樣也不好,

所以在hashMap中loadFactor的初始值就是0.75,一般情況下不需要更改它。

  2)桶

    數組中每一個位置上都放有一個桶,每個桶裏就是裝一個鏈表,鏈表中可以有很多個元素(entry),這就是桶的意思。也就相當於把元素都放在桶中。

  3)capacity

    capacity譯為容量代表的數組的容量,也就是數組的長度,同時也是HashMap中桶的個數。默認值是16。

  4)size的含義

    size就是在該HashMap的實例中實際存儲的元素的個數

  5)threshold的作用

    threshold = capacity * loadFactor,當Size>=threshold的時候,那麽就要考慮對數組的擴增了,也就是說,這個的意思就是衡量數組是否需要擴增的一個標準

    註意這裏說的是考慮,因為實際上要擴增數組,除了這個size>=threshold條件外,還需要另外一個條件。

    什麽時候會擴增數組的大小?在put一個元素時先size>=threshold並且還要在對應數組位置上有元素,這才能擴增數組。

二、HashMap的源碼分析

2.1、HashMap的層次關系與繼承結構

  1)HashMap繼承結構

    技術分享圖片

  上面就繼承了一個abstractMap,也就是用來減輕實現Map接口的編寫負擔。

  2)實現接口

    技術分享圖片

    Map<K,V>:在AbstractMap抽象類中已經實現過的接口,這裏又實現,實際上是多余的。但每個集合都有這樣的錯誤,也沒過大影響

    Cloneable:能夠使用Clone()方法,在HashMap中,實現的是淺層次拷貝,即對拷貝對象的改變會影響被拷貝的對象。

    Serializable:能夠使之序列化,即可以將HashMap對象保存至本地,之後可以恢復狀態。

2.2、HashMap類的屬性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列號
    private static final long serialVersionUID = 362498820763181265L;    
    // 默認的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默認的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹
    static final int TREEIFY_THRESHOLD = 8; 
    // 當桶(bucket)上的結點數小於這個值時樹轉鏈表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中結構轉化為紅黑樹對應的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存儲元素的數組,總是2的冪次倍
    transient Node<k,v>[] table; 
    // 存放具體元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的個數,註意這個不等於數組的長度。
    transient int size;
    // 每次擴容和更改map結構的計數器
    transient int modCount;   
    // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容
    int threshold;
    // 填充因子
    final float loadFactor;
}

2.3、HashMap的構造方法

  有四個構造方法,構造方法的作用就是記錄一下16這個數給threshold(這個數值最終會當作第一次數組的長度。)和初始化加載因子。註意,hashMap中table數組一開始就已經是個沒有長度的數組了。

  構造方法中,並沒有初始化數組的大小,數組在一開始就已經被創建了,構造方法只做兩件事情,一個是初始化加載因子,另一個是用threshold記錄下數組初始化的大小。註意是記錄。

  技術分享圖片

三、HashMap源碼分析(二)

3.1、put方法

   /* 
* 1. 通過key的hash值確定table下標
* 2. 查找table下標,如果key存在則更新對應的value
* 3. 如果key不存在則調用addEntry()方法
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}

if (key == null)
return putForNullKey(value);

int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 哈希碼相同並且對象相同時
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 新值替換舊值,並返回舊值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// key不存在時,加入新元素
modCount++;
addEntry(hash, key, value, i);
return null;
}

3.2、get方法

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}

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

3.3、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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

四、總結

4.1、關於數組擴容

  從putVal源代碼中我們可以知道,當插入一個元素的時候size就加1,若size大於threshold的時候,就會進行擴容。假設我們的capacity大小為32,loadFator為0.75,則threshold為24 = 32 * 0.75,

  此時,插入了25個元素,並且插入的這25個元素都在同一個桶中,桶中的數據結構為紅黑樹,則還有31個桶是空的,也會進行擴容處理,其實,此時,還有31個桶是空的,好像似乎不需要進行擴容處理,

  但是是需要擴容處理的,因為此時我們的capacity大小可能不適當。我們前面知道,擴容處理會遍歷所有的元素,時間復雜度很高;前面我們還知道,經過一次擴容處理後,元素會更加均勻的分布在各個桶中,

  會提升訪問效率。所以,說盡量避免進行擴容處理,也就意味著,遍歷元素所帶來的壞處大於元素在桶中均勻分布所帶來的好處。 

4.2、總結

  1)要知道hashMap在JDK1.8以前是一個鏈表散列這樣一個數據結構,而在JDK1.8以後是一個數組加鏈表加紅黑樹的數據結構。

  2)通過源碼的學習,hashMap是一個能快速通過key獲取到value值得一個集合,原因是內部使用的是hash查找值得方法。

Java集合源碼分析--HashMap