39-45行 註釋:主要介紹了HashMap和HashTable的區別,即HashMap允許null作爲鍵、值,而且HashMap不是線程安全的。並且HashMap中的元素不是有序的,特別的,也不保證隨着時間推移,這個map中存儲的順序不發生改變。
解析:hashmap是線程不安全的,鍵、值都允許null的存在,map中的元素不保證有序,隨着時間推移可能還會發生變化。
47-54行 註釋:HashMap的get、put方法在能夠正確將元素散列在桶中的情況下擁有常數級別的表現(即碰撞次數不過多)。而迭代器在Map中的表現情況依賴於初始化的容量與桶的數量+鍵值對的值的比例,即裝載因子。因此,如果希望迭代器的表現良好,則不能夠設置過大的初始容量。
我們知道HashMap的方法中,默認的裝載因子大小是0.75,初始容量大小是16(旁邊還有一句註釋,必須是2的次方)
解析:默認裝載因子是0.75,默認初始容量是16
56-65行 註釋:就像上面提到的,影響HashMap性能的主要有兩個因素,一個是初始容量的大小,一個是裝載因子的大小。當HashMap中鍵值對的數量超過當前容量大小*裝載因子的時候,整個表會重新進行hash一次,並進行擴容(接近於原容量的兩倍)。
解析:每次resize的大小是將原容量擴充接近1倍。
67-76行 註釋: 介紹了採用0.75作爲默認裝載因子的意義。太高的裝載因子會減少多餘的空間,但是對查找性能很不友好。因此,最好是在初始化HashMap的時候根據它的鍵值對量對初始容量和裝載因子進行相應的調整,確保讓整個hashmap重新hash的次數最小化(即提高性能)。如果初始容量*裝載因子>鍵值對的數量,那麼重新散列的情況就不會發生。
解析:定
78-85行 註釋:如果Hashmap中的大多數鍵值對都已經有序了,那麼給與它一個充分大的容量會比小的好(肯定啊。。。小了又要rehash)。如果說map中存放了大量的重複的鍵,會影響map的效率。爲了改善影響,那麼會在鍵之間採用比較來改善這種情況。
解析:hashmap的散列方法採用的是拉鍊法,如果有大量重複主鍵的話,必然會導致碰撞的大量發生。
87-94行 註釋:介紹了hashmap的一個特點:線程不安全。如果有多個線程對Map進行增加/刪除元素的話,需要在外部對map對象加鎖。這個通常是由一些對象封裝了相應的映射關係來完成的。
解析:hashmap是線程不安全的,如果有併發操作需要在外部加上互斥鎖。
96-100行 註釋:接着上面的繼續講,如果不存在這種封裝了的對象,那麼hashmap需要在初始化之初使用一個包裝類:Collections.synchronizedMap
操作像這樣:Map m = Collections.synchronizedMap(new HashMap(...));
解析:一種使線程不安全的hashmap變得線程安全的操作。
102-109行 註釋:這裏介紹了一下集合類迭代器的一個共同特性:快速失敗。即當迭代器創建之後,所屬的集合有任何結構上的變化(比如增加/刪除元素,那種改變已有鍵值對的不算),都會拋出一個名爲:ConcurrentModificationException的異常。除非是使用迭代器本身安全的remove()方法。
解析:迭代器本身有兩個元素,一個ModCount,一個ExceptModCount,每次移動迭代器指針都會檢查這兩個的值是否相等,如果不等就會拋出快速失敗的異常,而迭代器本身的人remove()方法會同步修改這兩個值,則不會拋出異常。這個是所有集合類迭代器的共同特徵。
110-117行 註釋:迭代器的快速失敗特徵是不可靠的,應該說任何依賴於不同步情況下的併發修改都是難以保證正確性的。因此,迭代器的快速失敗特徵應該只被用於檢查bug出在哪裏,而不是保證程序的正確性。
145-154行 註釋:hashmap雖然在很多實現的性能表現地像哈希表元素存儲在桶裏,但在map中桶的數量過多時,桶節點會轉化成二叉樹節點(紅黑樹)我們知道紅黑樹是平衡樹的一種,它在數據很大時的查找性能很好。當然我們知道,方法大多數還是爲一般情況下準備的,因此這種檢驗桶是否變成了樹節點會一定程度上影響性能。
解析:HashMap在桶(一個單鏈表,因爲採用的是拉鍊法)的鍵簇(即鏈表中的元素數目)較大時(這個值是8),桶節點會變成樹節點(紅黑樹),用於提高它的查找性能。但同時這種檢驗也難免會一定程度上影響平時的性能。(最好的查找性能是一個桶裏只有一個元素,因爲桶裏存放的是發生碰撞的)
156-172行 註釋:當桶裏的節點全部變成樹節點的時候,它們會主要通過比較hashcode變得有序。如果兩個鍵之間都是實現了Comparable接口的(比如常用的原始數據類型+包裝類+String都是實現了的),那麼它們會通過compareTo()方法進行比較。雖然說這樣對於本來hashcode就唯一/已經有序的鍵值對會比較浪費時間,但是對於hashcode()方法錯誤的分配以及很多鍵共享同一個hashcode的情況,這麼做是值得的。
解析:對於樹節點中的數據,hashmap主要採用比較Hashcode的方法,如果鍵的類型實現了Comparable接口的方法,那麼就會採用compareTo()使它有序。我們知道,hashcode相等,不一定equals(),equals()不一定==。
174-186行 註釋:因爲樹節點佔用的空間比較大,近似普通節點的2倍,所以只有當空間足夠的情況下才會進行轉變(>=8),在小於6的時候又會轉變回去。如果hashcode方法分配良好,在理想情況下,桶中轉變爲樹節點的概率應當服從泊松分佈。
解析:轉化爲樹節點的閾值是8,退化的閾值是6.樹節點佔用空間較大。
199-202行 註釋:根節點有時候可能不在樹中(比如迭代器的remove方法),不過可以通過TreeNode.root()方法恢復。
204-209行 註釋:所有適用的內部方法接收哈希碼作爲一個參數(通常來自公有方法),來避免對鍵哈希碼的重新計算。許多內部方法也接收一個標籤參數,通常是當前的表,在resize的時候也能是新的或舊的表。
解析:接收哈希碼作爲參數,避免重複計算。提高性能。
211-218行 註釋:當哈希桶成樹型/非樹型以及分隔時,我們會保持它們處在同一種遍歷順序當中,並且一定程度上(較小)減輕迭代器操作的負擔。在使用比較和插入的時候,爲了保持總體的跨平衡有序,我們把鍵的類型和獨有的hashcode作爲連接橋樑。
220-226行 註釋:桶是樸素桶還是紅黑樹桶的使用和轉換,由於子類LinkedHashMap的存在而變得更加的複雜。hashmap中的一些由添加/刪除觸發得到鉤子方法允許LinkedHashMap在其他情況下保留獨立的內部特性。(筆者找到的鉤子方法中的一個是void reinitialize()方法)它們也需要map對象實例通過一些實例方法創建新的節點
(
228-229行 註釋:併發編程使用類似於SSA的風格,可以減少錯誤。(SSA:筆者查了一下,這是一種應用在JVM中的對代碼的編譯方法)
以上是hashmap的一些特性,接下來是源碼的逐行分析閱讀
- hashCode()方法:將key的哈希碼與value的哈希碼做異或運算得到。
- setValue(V newValue):返回修改前的(舊的)Value值
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
解析:即普通的獲取map中的鍵值對映射的值,不過注意,因爲hashmap允許鍵、值爲null,所以get方法返回Null並不能作爲不存在相對應鍵的判定,而是應該採用containsKey()方法來達到這個效果。final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
解析:該方法是HashMap的核心方法之一。first是對應的key值對應的hash值,在table數組中對應hash值位置鏈表的第一個元素。如果要找的key就是這個first,則將該first鍵值對返回。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
解析:本方法採用566行實例方法,這也是爲什麼containsKey能判斷鍵是否存在。因爲getNode不會返回Null,除非查找的元素不存在。 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
解析:這是HashMap中最重要的方法之一。接收四個參數,其中後兩個布爾型,一個代表在所對應鍵的值已經存在的情況下,是否執行覆蓋;一個代表hashMap的table字段是否處於創建狀態(前面提到,table僅在初始化時執行一次(不是終態是因爲包括resize操作))。而常用的Put()方法,調用時是採取覆蓋操作,不創建table.676行 終態方法: final Node<K,V>[] resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//留在原數組索引位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//挪到新位置,原索引+oldCap(n)
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
解析:這是HashMap中核心方法之一,用於拓展map中鍵值對數組的大小(拉鍊法的數組)。如果還沒有被初始化,那麼table數組會被初始化爲默認大小(16),否則會拓展爲原大小的兩倍。鏈表中的元素,因爲是產生“碰撞”而進去的,所以會有相同的table索引值(table[(table.length-1)&hash]);或者會分散在偏移量爲2的乘方的新的table數組中if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
吶,可以看到,如果鏈表裏面只有一個節點(即沒有發生碰撞),那肯定是樸素桶了,直接重新計算索引值搬家即可;否則,發生了碰撞,判斷當前節點是不是樹節點,如果是,執行樹節點的分散方法。(注:本方法只會被resize方法調用)- 1.8中沒有rehash獲得新的hash值再將newHash&(newCap-1),而是在原基礎上利用原hash/容量,將鏈表中元素分成了要遷移和不遷移的兩部分
- 1.8中resize方法保全了鏈表中原順序的有序性,1.7則將原鏈表順序倒置
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}