【java提高】---HashMap解析(一)
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解析(一)