- 參考博客
Java中的equals和hashCode方法詳解
鏈表
java提高篇(二三)—–HashMap Java
HashMap的工作原理
算法的時間複雜度和空間複雜度-總結
Hashmap
Hash (散列函數)
第1部分 HashMap介紹
HashCode的作用原理和實例解析
hash碰撞處理
高性能場景下,HashMap的優化使用建議
- 整體思路
1. 什麼是HashMap?有什麼作用?
2. 瞭解HashMap原理前,需要知道哪些知識?
3. 分析HashMap原理。
4. 應用場景。
- 什麼是HashMap?有什麼作用?
什麼是HashMap: 基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。 此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關係數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高(或將加載因子設置得太低)。
作用:通過哈希算法,通過鍵值可快速找到自己需要的對象。
- 瞭解HashMap原理前,需要知道哪些知識??
- 集合
- HashMap相關接口
- 哈希算法
- 散列(哈希)表
- 鏈表
- 散列碰撞
算法複雜度計算
集合:集合類存放的都是對象的引用,而非對象本身,出於表達上的便利,我們稱集合中的對象就是指集合中對象的引用(reference)。集合類型主要有3種:set(集)、list(列表)和map(映射)。
- Map相關接口:HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。
HashMap的構造函數:
HashMap() // 默認構造函數
HashMap(int capacity) // 指定“容量大小”的構造函數
HashMap(Map ? extends K, ? extends V> map) // 包含“子Map”的構造函數
HashMap(int capacity, float loadFactor) // 指定“容量大小”和“加載因子”的構造函數
Entry: 存儲數據的Entry數組,長度是2的冪。HashMap是採用拉鍊法實現的,每一個Entry本質上是一個單向鏈表 ,如圖可以很好的解釋哈希結構:
每次新建一個HashMap時,都會初始化一個table數組。table數組的元素爲Entry節點哈希算法
哈希算法:哈希算法將任意長度的二進制值映射爲較短的固定長度的二進制值,這個小的二進制值稱爲哈希值。哈希值是一段數據唯一且極其緊湊的數值表示形式。如果散列一段明文而且哪怕只更改該段落的一個字母,隨後的哈希都將產生不同的值。要找到散列爲同一個值的兩個不同的輸入,在計算上是不可能的,所以數據的哈希值可以檢驗數據的完整性。一般用於快速查找和加密算法。[參考知乎第一條答案](https://www.zhihu.com/question/20820286),比如這裏有一萬首歌,給你一首新的歌X,要求你確認這首歌是否在那一萬首歌之內。無疑,將一萬首歌一個一個比對非常慢。但如果存在一種方式,能將一萬首歌的每首數據濃縮到一個數字(稱爲哈希碼)中,於是得到一萬個數字,那麼用同樣的算法計算新的歌X的編碼,看看歌X的編碼是否在之前那一萬個數字中,就能知道歌X是否在那一萬首歌中。
hasCode方法:從Object角度看,JVM每new一個Object,它都會將這個Object丟到一個Hash表中去,這樣的話,下次做Object的比較或者取這個對象的時候(讀取過程),它會根據對象的HashCode再從Hash表中取這個對象。這樣做的目的是提高取對象的效率。若HashCode相同再去調用equal。
hash方法:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}indexFor方法(落桶):對於HashMap的table而言,數據分佈需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太鬆,太緊會導致查詢速度慢,太鬆則浪費空間。計算hash值後,怎麼才能保證table元素分佈均與呢?我們會想到取模,但是由於取模的消耗較大,HashMap是這樣處理的:調用indexFor方法。
static int indexFor(int h, int length) {
return h & (length-1);
}
table數組長度:hashmap中默認的數組大小是多少,查看源代碼可以得知是16,爲什麼是16,而不是15,也不是20呢,顯然是因爲16是2的整數次冪的原因,在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。哈希(散列)表
哈希表:散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
哈希函數:(鏈地址法hashmap使用)採用數組和鏈表相結合的辦法,將Hash地址相同的記錄存儲在一張線性表中,而每張表的表頭的序號即爲計算得到的Hash地址
鏈表
結構:每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不同於這些數據項目在記憶體或磁盤上順序,數據的存取往往要在不同的排列順序中轉換。鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取散列碰撞
對象Hash的前提是實現equals()和hashCode()兩個方法,那麼HashCode()的作用就是保證對象返回唯一hash值,但當兩個對象計算值一樣時,這就發生了碰撞衝突。
衝突解決:前提是hash一致性的情況。hash函數相當於,把原空間的一個數據集映射到另外一個空間。四種方式可以查看這個。
滿足三個原則
增大 映射空間/原空間 的大小
儘可能把原數據集均勻映射到較小空間
結合原空間數據的數據特徵
- 原理分析
- 創建一個測試類,創建一個hashmap隨機插入一個數據,打入斷點查看hashmap的結構
1.有一個table數組,長度爲16,隨機存放之前put進去的key,value對象,這個對象的名稱叫Entry
2.Entry包含了四個對象,hash,key,next,value
創建一個對象EntryTest,重寫他的hashCode方法,直接寫死讓所有的返回值都是1
隨機插入幾個數據,查看hashmap結構
這個時候,雖然插入3個對象table的數據長度爲1,點開Entry中的next屬性發現每個next都存放着一個Entry對象,這便是鏈表形式的存儲數據,hashmap採用的是哈希函數中拉鍊法存儲數據,數組+鏈表。查看源碼中的addEntry方法:
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);
//若HashMap中元素的個數超過極限了,則容量擴大兩倍
if (size++ >= threshold)
resize(2 * table.length);
}
瞭解基本的hashmap結構,我們主要從拉鍊法來分析hashmap的put和get方法。查看put源碼:
/**
* Associates the specified value with the specified key in this map. If the
* map previously contained a mapping for the key, the old value is
* replaced.
*
* @param key
* key with which the specified value is to be associated
* @param value
* value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or <tt>null</tt>
* if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return
* can also indicate that the map previously associated
* <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
1.對key做null的檢查,如果爲null,會被存儲在table[0]中,因爲null的hash值總是爲0
2.計算好hash值,hash值即爲存在table數組中的索引,爲了考慮到table數組的均勻分配,最好能保證每個裏面都能分配到一個,增加查詢速度
3.indexFor(hash,table.length)用來計算在table數組中存儲Entry對象的精確的索引,HashMap的底層數組長度總是2的n次方,在構造函數中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層數組長度爲2的n次方。當length爲2的n次方時,h&(length - 1)就相當於對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個優化。至於爲什麼是2的n次方點擊這篇博客。
4.通過hash算法可以知道,如果2個key有相同的hash值,此爲hash碰撞,相同的hash值會以鏈表的形式存儲:
如果在剛纔計算出來的索引位置沒有元素,直接把Entry對象放在那個索引上。
如果索引上有元素,然後會進行迭代,一直到Entry->next是null。當前的Entry對象變成鏈表的下一個節點。
如果我們再次放入同樣的key會怎樣呢?邏輯上,它應該替換老的value。事實上,它確實是這麼做的。在迭代的過程中,會調用equals()方法來檢查key的相等性(key.equals(k)),如果這個方法返回true,它就會用當前Entry的value來替換之前的value。
這裏要注意兩個問題:
一是鏈的產生。這是一個非常優雅的設計。系統總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。
二、擴容問題。 隨着HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的鏈表長度就會越來越長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子,默認是0.75也就是說16*0.75=13就開始擴容了。但是擴容是一個非常耗時的過程,因爲它需要重新計算這些數據在新table數組中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。
- bucketIndex相當於緩衝池的作用,避免了每次產生Entry對象是都去操作table數組,對象在產生的時候只需要去關心bucketIndex位置的元素就可以完成整個數組元素的插入和創建
- 爲了防止鏈表的長度過長,系統會設置臨界點進行擴容,臨界點爲table數組長度*加載因子。但是擴容是一個非常耗時的過程如果我們預知hashmap中的元素進行設計就能有效提高hashmap性能
put方法的實現還是圍繞數組加鏈表的形式,需要注意的是碰撞和擴容的問題。接下來是存儲的問題,查看put方法的實現:
public V get(Object key) {
// 若爲null,調用getForNullKey方法返回相對應的value
if (key == null)
return getForNullKey();
// 根據該 key 的 hashCode 值計算它的 hash 碼
int hash = hash(key.hashCode());
// 取出 table 數組中指定索引處的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key與查找的key相同,則返回相對應的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
圍繞數組加鏈表結構,通過key的hash值找到table數組中的Entry ,key是以String的形式存儲在Entry對象中的,只需要使用equals方法匹配到對應鏈表中的key取到value即可獲取到value的值:
- 應用場景
- 如何優化
1.考慮加載因子地設定初始大小
2.減小加載因子
3.String類型的key,不能用==判斷或者可能有哈希衝突時,儘量減少長度
4.使用定製版的EnumMap
5.使用IntObjectHashMap
優化從提高查詢效率和減少系統開銷分析。
加載因子存在的原因,還是因爲減緩哈希衝突,如果初始桶爲16,等到滿16個元素才擴容,某些桶裏可能就有不止一個元素了。所以加載因子默認爲0.75,也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。
對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;
如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。