1. 程式人生 > >HashMap源代碼閱讀理解

HashMap源代碼閱讀理解

none oat 進行 內容 多少 1.0 null val 數字

前言:
這個實現跟數組和鏈表相關。應用了他們各自的特點,進行優化。因為之前的很多工作經驗都是.NET的,所以之前都研究了.NET的東西。在寫了一陣子java之後,熟悉了java,就開始寫一些java的東西。
做一門語言,了解底層的實現一直是我的追求。畢竟了解了他們的實現,你才會了解到到底什麽樣的情況下,該用什麽樣的集合。
數組的優勢是,方便查找,但是新增超過加載因子的閾值的話(這部分是hashmap初始化的時候賦值進去的,如果不賦值,則會給默認值),就需要重新分配。鏈表查找比較麻煩,順子鏈子一直找,找到位置才截止,有多少就得找多少。
數組的時間復雜度為O(1),鏈表的時間復雜度為O(n)。因為數組是根據下標來找的,而數組的大小是確定的。而鏈表的大小是不確定的,是根據有多少元素進行多少次查找,找到為止。
於是就會有人說hash碰撞,什麽是hash碰撞,根據key(這裏的key是根據散列值計算出來的)找數組的時候,發現數組的下標已經有值了,有值的情況下,會走鏈表的對比。所以會有碰撞的情況下,時間復雜度為O(n)的說法。
不過看了源代碼的收獲還是蠻大的,很多寫法值得借鑒。因為裏面的很多變量都是一個英文字母之類的,因此可讀性比較差。但是我想,為了性能考慮的話,這一切都是值得的。畢竟源代碼的性能高於可閱讀性。所以就造成了我們閱讀源代碼的時候有點吃力。
文章比較枯燥,很多都是個人見解。如果哪裏錯了,望指出來共同進步。後面有時間了,也會寫一些其他的集合類型的源碼理解。因為這對於代碼的優化也有借鑒作用,謝謝大家!
正文:
先看下構造函數部分,用來初始化的部分,其實初始化也很重要。容量和加載因子最好設置在合適的範圍。過小的話,會不斷的resize。過大的話又顯得太過於浪費。因此根據需求來初始化一個合適的值,顯得還是很重要的

無初始化參數
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 加載因子默認0.75f
}

根據m初始化
public HashMap(Map<? extends K, ? extends V> m) {//初始化並加入m的內容
this.loadFactor = DEFAULT_LOAD_FACTOR; // 加載因子默認0.75f
putMapEntries(m, false);//@1
}

public HashMap(int initialCapacity, float loadFactor) { //初始化hashmap的容量和加載因子
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); //根據容量和加載因子得到閾值 @2
}
看完了初始化,再來看看put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);//@3 @4
}
再來看看取值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;//@5
}

--------------------------------------------------------------------------------方法說明--------------------------------------------------------------------------------------
/**
* @1把m賦值到初始化的hashMap裏
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { //初始化的時候,哈希桶是空的,所以走這段邏輯
float ft = ((float)s / loadFactor) + 1.0F; //如果我們傳的m的size是6,6/0.75 + 1 = 9 為什麽+1 我個人的理解就是先+1,看看是否超過了閾值,如果超過了閾值,就擴容,不加1的話,如果正好等於閾值就不會擴容。如果我們這一步不加1,那麽tableSizeFor(8)求出來的就是8,那麽正好等於閾值。但是沒進行擴容。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);//MAXIMUM_CAPACITY是最大的容量,為2的30次方,因此這裏的t就是9
       if (t > threshold)//threshold是容量閾值,超過這個閾值後就會resize() 這個時候是0
        threshold = tableSizeFor(t);//求出閾值@2
      }
else if (s > threshold) //如果不是初始化的時候,比較size是否大於閾值,大於則重新分配
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);
}
}
}

/**@2
* 返回大於輸入參數且最近的2的整數次冪的數
* 這裏網上查的都是大於,但是我發現,如果傳入的是2的冪次方,那麽就會得出2的冪次方這個數,比如8 16等,所以我個人的理解是大於等於,不知道是不是我哪裏理解錯了
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;//9-1=8 防止9是2的冪次方的數,如果9是2的冪次方 則會
n |= n >>> 1;//無符號右移,無論正負,高位補0 0000 1000|=0000 0100=0000 1100=12
n |= n >>> 2;//0000 1100|=0000 0011=0000 1111=15
n |= n >>> 4;//0000 1111|=0000 0000=0000 1111=15
n |= n >>> 8;//0000 1111|=0000 0000 =0000 1111=15
n |= n >>> 16;//0000 1111|=0000 0000=0000 1111 =15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//return 16
}
/**
*@3
*java裏的hashMap用的是數組和鏈表的結合,數組尋址比較容易(下標易找),但是插入和刪除不方便(需要重新排列)。鏈表查找不方便(需要順著鏈子找),但是插入和刪除比較方便(把next和pre指針變動下就可以了)
*數組中存著指針,指向鏈表中的元素(哈希桶),而我們hash函數就負責定位到下標。
*從這裏我們就可以看出 如果我們沒有遇到哈希碰撞,那麽時間復雜度就是O(1) 如果遇到碰撞了,時間復雜度就是O(n)
*/
static final int hash(Object key) {//hash叫做散列,就是把任意長度的輸入,通過散列算法,變成固定長度的輸出(散列值)
int h;// 散列會有碰撞,比如我現在只有0和1兩個數字,要變成1位,那麽輸入的組合為00 01 10 11 輸出的組合為0 1,所以不同的對象就會有相同的散列。這個時候同性相斥,就打起來了
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//返回散列值
}

//**@4
* Implements Map.put and related methods
*
* @param hash 散列值
* @param key hashMap的key
* @param value 需要put的hashMap的value
* @param onlyIfAbsent true存在key的話,就忽略不覆蓋,false,相同key,覆蓋原來值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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;//重新分配後的length
if ((p = tab[i = (n - 1) & hash]) == null)//根據最大的索引值和hash來求得下標,看是否會有碰撞
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))))//如果跟放在鏈表第一個的對象相同的key和相同的value,則賦值給e
e = p;
else if (p instanceof TreeNode)//如果值是treenode類型,則走treenode的puttreevalue方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//走鏈表,看看下個元素是否為空,為空則放到下個元素裏面去,註意,走到最後e是空的
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))))//如果遇到相同的key和value(代表放進去了)則break
break;
p = e;//走鏈表,一個個順下去
}
}
if (e != null) { //這個key相應的value已經存在情況下,上面if else第一種情況
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//如果遇到相同的值,是否替換原值
e.value = value;
afterNodeAccess(e);//把e移到鏈表的最後一個
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}}

/**@5
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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) {//我們之前putvalue的方法講過的,(n-1)&hash是用來計算hash桶的下標的,找到下標
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))//這個putvalue也有相應的方法和解釋
return first;//返回
if ((e = first.next) != null) {//查看下一個
if (first instanceof TreeNode)//如果不是linkedHashMap,而是TreeNode,
return ((TreeNode<K,V>)first).getTreeNode(hash, key);//返回TreddNode的計算方式的值
do {//順著鏈子找到相應的對象,返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

HashMap源代碼閱讀理解