以前很早就在博客中寫過HashMap的一些東西:
完全搞懂HashMap,HashTableConcurrentHashMap關聯:
HashMap和HashTable的區別:
今天來說HashMap是分JDK7和JDK8 對比着來說的, 由於JDK8中針對於HashMap有些小的改動, 這也是一些面試會常常問到的點。
一:JDK7中的HashMap:
HashMap底層維護一個數組table, 數組中的每一項是一個key,value形式的Entry。
咱們往HashMap中所放置的對象實際是存儲在該數組中。
Map中的key,value則以Entry的形式存放在數組中。
這個Entry應該放在數組的哪個位置上, 是經過key的hashCode來計算的。這個位置也成爲hash桶。
經過hash計算出來的值將經過indexFor方法找到它所在的table下標:
這個方法實際上是對table.length取模,
當兩個key經過
hashCode計算相同時,則發生了hash衝突(碰撞),HashMap解決hash衝突的方式是用鏈表。當發生hash衝突時,則將存放在數組中的Entry設置爲新值的next(這裏要注意的是,好比A和B都hash後都映射到下標i中,以前已經有A了,當map.put(B)時,將B放到下標i中,A則爲B的next,因此新值存放在數組中,舊值在新值的鏈表上)。
例如上圖, 一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。它的內部實際上是用一個Entity數組來實現的,屬性有key、value、next。
接着看看put方法:
469行, 若是key爲空, 則把這個對象放到第一個數組上。
471行, 計算key的hash值
472行, 經過indexFor方法返回分散到數組table中的下標
473行, 經過table[i]獲取新Entry的值, 若是值不爲空,則判斷key的hash值和equals來判斷新的Entry和舊的Entry值是否相同, 若是相同則覆蓋舊Entry的值並返回。
484行, 往數組上添加新的Entry。
添加Entry時,當table的容量大於theshold(
(int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)), 這裏實際上就是16*0.75=12
如上, 當知足必定條件後 table就開始擴容, 這個過程也稱爲rehash, 具體請看下圖:
559行: 建立一個新的Entry數組
564行: 將數組轉移到新的Entry數組中
565行: 修改resize的條件threshold
再具體的實現你們能夠看下jdk7中HashMap的相關源碼。
二:JDK8中的HashMap:
一直到JDK7爲止,HashMap的結構都是這麼簡單,基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式存儲。
這樣子的HashMap性能上就抱有必定疑問,若是說成百上千個節點在hash時發生碰撞,存儲一個鏈表中,那麼若是要查找其中一個節點,那就不可避免的花費O(N)的查找時間,這將是多麼大的性能損失。這個問題終於在JDK8中獲得瞭解決。再最壞的狀況下,鏈表查找的時間複雜度爲O(n),而紅黑樹一直是O(logn),這樣會提升HashMap的效率。
JDK7中HashMap採用的是位桶+鏈表的方式,即咱們常說的
散列鏈表的方式,而JDK8中採用的是位
桶+鏈表/紅黑樹的方式,也是非線程安全的。當
某個位桶的鏈表的長度達到某個閥值的時候,這個鏈表就將轉換成紅黑樹。
JDK8中,當同一個hash值的節點數大於等於8時,將再也不以單鏈表的形式存儲了,會被調整成一顆紅黑樹(上圖中null節點沒畫)。這就是JDK7與JDK8中HashMap實現的最大區別。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//若是當前map中無數據,執行resize方法。而且返回n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//若是要插入的鍵值對要存放的這個位置恰好沒有元素,那麼把他封裝成Node對象,放在這個位置上就完事了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//不然的話,說明這上面有元素
else {
Node<K,V> e; K k;
//若是這個元素的key與要插入的同樣,那麼就替換一下,也完事。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//1.若是當前節點是TreeNode類型的數據,執行putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//仍是遍歷這條鏈子上的數據,跟jdk7沒什麼區別
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//2.完成了操做後多作了一件事情,判斷,而且可能執行treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //true || --
e.value = value;
//3.
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判斷閾值,決定是否擴容
if (++size > threshold)
resize();
//4.
afterNodeInsertion(evict);
return null;
}
treeifyBin()就是將鏈表轉換成紅黑樹。
以前的indefFor()方法消失 了,直接用(tab.length-1)&hash,因此看到這個,表明的就是數組的角標。
具體紅黑樹的實現你們能夠看下JDK8中HashMap的實現。
三:須要注意的地方:
再談HashCode的重要性
前面講到了,HashMap中對Key的HashCode要作一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode,那麼爲何要防止糟糕的HashCode?
糟糕的HashCode意味着的是Hash衝突,即多個不一樣的Key可能獲得的是同一個HashCode,糟糕的Hash算法意味着的就是Hash衝突的機率增大,這意味着HashMap的性能將降低,表如今兩方面:
一、有10個Key,可能6個Key的HashCode都相同,另外四個Key所在的Entry均勻分佈在table的位置上,而某一個位置上卻鏈接了6個Entry。這就失去了HashMap的意義,HashMap這種數據結構性高性能的前提是,Entry均勻地分佈在table位置上,但如今確是1 1 1 1 6的分佈。因此,咱們要求HashCode有很強的隨機性,這樣就儘量地能夠保證了Entry分佈的隨機性,提高了HashMap的效率。
二、HashMap在一個某個table位置上遍歷鏈表的時候的代碼:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
看到,因爲採用了"&&"運算符,所以先比較HashCode,HashCode都不相同就直接pass了,不會再進行equals比較了。HashCode由於是int值,比較速度很是快,而equals方法每每會對比一系列的內容,速度會慢一些。Hash衝突的機率大,意味着equals比較的次數勢必增多,必然下降了HashMap的效率了。
HashMap的table爲何是transient的
一個很是細節的地方:
transient Entry[] table;
看到table用了transient修飾,也就是說table裏面的內容全都不會被序列化,不知道你們有沒有想過這麼寫的緣由?
在我看來,這麼寫是很是必要的。由於HashMap是基於HashCode的,HashCode做爲Object的方法,是native的:
public native int hashCode();
這意味着的是:HashCode和底層實現相關,不一樣的虛擬機可能有不一樣的HashCode算法。再進一步說得明白些就是,可能同一個Key在虛擬機A上的HashCode=1,在虛擬機B上的HashCode=2,在虛擬機C上的HashCode=3。
這就有問題了,Java自誕生以來,就以跨平臺性做爲最大賣點,好了,若是table不被transient修飾,在虛擬機A上能夠用的程序到虛擬機B上能夠用的程序就不能用了,失去了跨平臺性,由於:
一、Key在虛擬機A上的HashCode=100,連在table[4]上
二、Key在虛擬機B上的HashCode=101,這樣,就去table[5]上找Key,明顯找不到
整個代碼就出問題了。所以,爲了不這一點,Java採起了重寫本身序列化table的方法,在writeObject選擇將key和value追加到序列化的文件最後面:
private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
Iterator<Map.Entry<K,V>> i =
(size > 0) ? entrySet0().iterator() : null;
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
s.writeInt(table.length);
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (size > 0) {
for(Map.Entry<K,V> e : entrySet0()) {
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}
而在readObject的時候重構HashMap數據結構:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
// set hashSeed (can only happen after VM boot)
Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
sun.misc.Hashing.randomHashSeed(this));
// Read in number of buckets and allocate the bucket array;
s.readInt(); // ignored
// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
int initialCapacity = (int) Math.min(
// capacity chosen by number of mappings
// and desired load (if >= 0.25)
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
int capacity = 1;
// find smallest power of two which holds all mappings
while (capacity < initialCapacity) {
capacity <<= 1;
}
table = new Entry[capacity];
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init(); // Give subclass a chance to do its thing.
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
一種麻煩的方式,但卻保證了跨平臺性。
這個例子也告訴了咱們:儘管使用的虛擬機大多數狀況下都是HotSpot,可是也不能對其它虛擬機無論不顧,有跨平臺的思想是一件好事。
HashMap和Hashtable的區別
HashMap和Hashtable是一組類似的鍵值對集合,它們的區別也是面試常被問的問題之一,我這裏簡單總結一下HashMap和Hashtable的區別:
一、Hashtable是線程安全的,Hashtable全部對外提供的方法都使用了synchronized,也就是同步,而HashMap則是線程非安全的
二、Hashtable不容許空的value,空的value將致使空指針異常,而HashMap則無所謂,沒有這方面的限制
三、上面兩個缺點是最主要的區別,另一個區別可有可無,我只是提一下,就是兩個的rehash算法不一樣,Hashtable的是:
這個hashSeed是使用sun.misc.Hashing類的randomHashSeed方法產生的。HashMap的rehash算法上面看過了,也就是: