Java中HashMap的實作原理解析
這篇文章帶給大家的內容是關於Java中HashMap的實作原理解析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
1. HashMap概述:
HashMap是基於哈希表的Map介面的非同步實作。此實作提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
2. HashMap的資料結構:
在java程式語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來建構的,HashMap也不例外。 HashMap實際上是一個「鍊錶散列」的資料結構,即數組和鍊錶的結合體。
從上圖可以看出,HashMap底層就是一個陣列結構,陣列中的每一項又是一個鍊錶。當新建一個HashMap的時候,就會初始化一個陣列。
/** * The table, resized as necessary. Length MUST Always be a power of two. */ 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; …… }
3. HashMap的存取實作:
1) 儲存:
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值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鍊錶的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置。
addEntry(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 就隨之保存在那裡即可。
hash(int h)方法根據key的hashCode重新計算一次雜湊。此演算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
我們可以看到在HashMap中要找到某個元素,需要根據key的hash值來求對應陣列中的位置。如何計算這個位置就是hash演算法。前面說過HashMap的資料結構是陣列和鍊錶的結合,所以我們當然希望這個HashMap裡面的元素位置盡量的分佈均勻些,盡量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用再去遍歷鍊錶,這樣就大大優化了查詢的效率。
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程式呼叫 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,「模」運算的消耗還是比較大的,在HashMap中是這樣做的:呼叫 indexFor(int h, int length) 方法來計算該物件應該保存在 table 陣列的哪個索引處。 indexFor(int h, int length) 方法的程式碼如下:
static int indexFor(int h, int length) { return h & (length-1); }
這個方法非常巧妙,它透過h & (table.length -1) 來得到該物件的保存位,而HashMap底層陣列的長度總是2 的n 次方,這是HashMap在速度上的最佳化。在 HashMap 建構器中有以下程式碼:
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
這段程式碼保證初始化時HashMap的容量總是2的n次方,即底層陣列的長度總是為2的n次方。
當length總是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。
當數組長度為2的n次冪的時候,不同的key算得index相同的幾率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鍊錶,這樣查詢效率也就較高了。
根據上面put 方法的原始碼可以看出,當程式試圖將一個key-value對放入HashMap中時,程式首先根據該key 的hashCode() 傳回值決定該Entry 的儲存位置:如果兩個Entry 的key 的hashCode() 回傳值相同,那麼它們的儲存位置相同。如果這兩個 Entry 的 key 透過 equals 比較傳回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆寫。如果這兩個Entry 的key 透過equals 比較回傳false,新加入的Entry 會與集合中原有Entry 形成Entry 鏈,而且新加入的Entry 位於Entry 鏈的頭部-具體說明繼續看addEntry() 方法的說明。
(2)讀取
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方法在对应位置的链表中找到需要的元素。
3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
4. HashMap的resize(rehash):
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
5. HashMap的性能参数:
HashMap 包含如下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。
initialCapacity:HashMap的最大容量,即为底层数组的长度。
loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
HashMap的实现中,通过threshold字段来判断HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:
if (size++ >= threshold) resize(2 * table.length);
6. Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } }
在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:
注意到modCount声明为volatile,保证线程之间修改的可见性。
final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException();
在HashMap的API中指出:
所有HashMap類別的「collection 視圖方法」所傳回的迭代器都是快速失敗的:在迭代器建立之後,如果從結構上對映射進行修改,除非透過迭代器本身的remove 方法,其他任何時間任何方式的修改,迭代器都會拋出ConcurrentModificationException。因此,面對並發的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時間發生任意不確定行為的風險。
注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的並發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程式的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程式錯誤。
相關推薦:
以上是Java中HashMap的實作原理解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

hashmap的擴容機制是:重新計算容量,用新的陣列取代原來的陣列。重新計算原始數組的所有資料並插入一個新數組,然後指向新數組;如果數組在容量擴展前已達到最大值,則直接將閾值設為最大整數返回。

如何使用HashMap類別的put()方法將鍵值對插入到HashMap中HashMap是Java集合框架中的一個非常重要的類,它提供了一種儲存鍵值對的方式。在實際開發中,我們經常需要在HashMap中插入鍵值對,透過使用HashMap類別的put()方法可以輕鬆實現這一目標。 HashMap的put()方法的簽章如下:Vput(Kkey,Vvalue)

javaHashMap插入重複Key值要在HashMap中插入重複的值,首先要先弄清楚HashMap裡面是怎麼存放元素的。 put方法Map裡面存放的每一個元素都是key-value這樣的鍵值對,而且都是透過put方法進行新增的,而且相同的key在Map中只會有一個與之關聯的value存在。 put方法在Map中的定義如下。 Vput(Kkey,Vvalue);put()方法實作:首先hash(key)得到key的hashcode(),hashmap根據所得的hashcode找到要插入的位置所在的鏈,

Java文件解讀:HashMap類別的containsKey()方法用法詳解,需要具體程式碼範例引言:HashMap是Java中常用的資料結構,它提供了高效率的儲存和尋找功能。其中的containsKey()方法用來判斷HashMap中是否包含指定的鍵。本文將詳細解讀HashMap類別的containsKey()方法的使用方式,並提供具體的程式碼範例。一、cont

1.說明Map基本上可以使用HashMap,但是HashMap有一個問題,那就是迭代HashMap的順序不是HashMap放置的順序,就是無序。 HashMap的這個缺點往往會帶來麻煩,因為有些場景我們期待一個有序的Map,那就是LinkedHashMap。 2.區別實例publicstaticvoidmain(String[]args){Mapmap=newLinkedHashMap();map.put("apple","蘋果");map.put("

一、單例模式是什麼?單例模式是一種物件建立模式,它用於產生一個物件的具體實例,它可以確保系統中一個類別只產生一個實例。 Java裡面實作的單例是一個虛擬機器的範圍,因為裝載類別的功能是虛擬機器的,所以一個虛擬機器在透過自己的ClassLoad裝載實作單例類別的時候就會建立一個類別的實例。在Java語言中,這樣的行為能帶來兩大好處:1.對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷; 2.由於new操作的次數減少,因而對系統記憶體的使用頻率也會降低,這將減輕GC壓

JavaMap是Java標準函式庫中常用的資料結構,它以鍵值對的形式儲存資料。 Map的效能對於應用程式的運作效率至關重要,如果Map的效能不佳,可能會導致應用程式運作緩慢,甚至崩潰。 1.選擇合適的Map實作Java提供了多種Map實現,包括HashMap、TreeMap和LinkedHashMap。每種Map實作都有各自的優缺點,在選擇Map實作時,需要根據應用程式的特定需求來選擇合適的實作。 HashMap:HashMap是最常用的Map實現,它使用哈希表來儲存數據,具有較快的插入、刪除和查找速度

Java使用HashMap類別的putAll()函數將一個Map加入到另一個Map中Map是Java中常用的資料結構,用來表示鍵值對的集合。在Java的集合框架中,HashMap是一個常用的實作類別。它提供了putAll()函數,用於將一個Map添加到另一個Map中,以方便實現資料的合併和拷貝。本文將介紹putAll()函數的使用方法,並提供對應的程式碼範例。首先,
