1. 程式人生 > >【java提高】---HashMap解析(一)

【java提高】---HashMap解析(一)

最終 fin 大牛 原理 pan 初始 com oid math

HashMap解析(一)

平時一直再用hashmap並沒有稍微深入的去了解它,自己花點時間想往裏面在深入一點,發現它比arraylist難理解很多,好多東西目前還不太能理解等以後自己知識更加豐富在過來理解。

數據結構中有數組和鏈表來實現對數據的存儲,但這兩者基本上是兩個極端。

數組:數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難;

鏈表:鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)鏈表的特點是:尋址困難,插入和刪除容易

HashMap的數據結構

HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。看下面圖;來理解:

技術分享圖片

從上圖中可以看出,HashMap底層就是一個數組結構,只數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

transient Entry[] table;
 
static class Entry<K,V> implements Map.Entry<K,V> {
    
final K key; V value; Entry<K,V> next; final int hash; …… }

可以看出,Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。

HashMap的存取實現

存儲

public V put(K key, V value) {
    // HashMap允許存放null鍵和null值。
    
// 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。 if (key == null) return putForNullKey(value); // 根據key的keyCode重新計算hash值。 int hash = hash(key.hashCode()); // 搜索指定hash值在對應table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。 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; } } // 如果i索引處的Entry為null,表明此處還沒有Entry。 modCount++; // 將key、value添加到i索引處。 addEntry(hash, key, value, i); return null; }

從上面的源代碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標), 如果數組該位置上已經存放有其他元素了,那麽在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

ddEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 獲取指定 bucketIndex 索引處的 Entry  
    Entry<K,V> e = table[bucketIndex];
    // 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    // 如果 Map 中的 key-value 對的數量超過了極限
    if (size++ >= threshold)
    // 把 table 對象的長度擴充到原來的2倍。
        resize(2 * table.length);
}

當系統決定存儲HashMap中的key-value對時,完全沒有考慮Entry中的value,僅僅只是根據key來計算並決定每個Entry的存儲位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之後,value 隨之保存在那裏即可。

讀取

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))  
            return e.value;
    }
    return null;
}

有了上面存儲時的hash算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:

從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

歸納

簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,

當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,

也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

hashmap源碼解讀

HashMap有兩個參數影響其性能:初始容量加載因子。默認初始容量是16,加載因子是0.75。容量是哈希表中桶(Entry數組)的數量,初始容量只是哈希表在創建時的容量。

加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,通過調用 rehash 方法將容量翻倍。

HashMap中定義的成員變量如下:

 
static final int DEFAULT_INITIAL_CAPACITY = 16;// 默認初始容量為16,必須為2的冪  
  
static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量為2的30次方  
  
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默認加載因子0.75  
    
transient Entry<K,V>[] table;// Entry數組,哈希表,長度必須為2的冪  
  
transient int size;// 已存元素的個數  
  
int threshold;// 下次擴容的臨界值,size>=threshold就會擴容  
  
final float loadFactor;// 加載因子  

HashMap一共重載了4個構造方法,分別為:

HashMap()
構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap

HashMap(int initialCapacity)
構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap

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

HashMap(Map<? extendsK,? extendsV> m)
構造一個映射關系與指定 Map 相同的 HashMap

看一下第三個構造方法源碼,其它構造方法最終調用的都是它。

public HashMap(int initialCapacity, float loadFactor) {  
    // 參數判斷,不合法拋出運行時異常  
    if (initialCapacity < 0)  
        throw new IllegalArgumentException("Illegal initial capacity: " +  
                                           initialCapacity);  
    if (initialCapacity > MAXIMUM_CAPACITY)  
        initialCapacity = MAXIMUM_CAPACITY;  
    if (loadFactor <= 0 || Float.isNaN(loadFactor))  
        throw new IllegalArgumentException("Illegal load factor: " +  
                                           loadFactor);  
  
    // Find a power of 2 >= initialCapacity  
    // 這裏需要註意一下  
    int capacity = 1;  
    while (capacity < initialCapacity)  
        capacity <<= 1;  
  
    // 設置加載因子  
    this.loadFactor = loadFactor;  
    // 設置下次擴容臨界值  
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  
    // 初始化哈希表  
    table = new Entry[capacity];  
    useAltHashing = sun.misc.VM.isBooted() &&  
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
    init();  
}  

這篇博客就到這裏為止了。

附大牛博客:HashMap深度解析(一)

HashMap深度解析(二)

Java容器(四):HashMap(Java 7)的實現原理

圖解集合 4 :HashMap

水滴石穿,成功的速度一定要超過父母老去的速度! 少尉【5】




【java提高】---HashMap解析(一)