1. 程式人生 > >hashmap深入分析

hashmap深入分析

 java.util.HashMap是很常見的類,前段時間公司系統由於對HashMap使用不當,導致cpu百分之百,在併發環境下使用HashMap 而沒有做同步,可能會引起死迴圈,關於這一點,sun的官方網站上已有闡述,這並非是bug。

HashMap的資料結構
         HashMap主要是用陣列來儲存資料的,我們都知道它會對key進行雜湊運算,哈系運算會有重複的雜湊值,對於雜湊值的衝突,HashMap採用連結串列來解決的。在HashMap裡有這樣的一句屬性宣告:
transient Entry[] table;
Entry就是HashMap儲存資料所用的類,它擁有的屬性如下
final K key;
V value;
final int hash;
Entry<K,V> next;


看到next了嗎?next就是為了雜湊衝突而存在的。比如通過雜湊運算,一個新元素應該在陣列的第10個位置,但是第10個位置已經有Entry,那麼好吧,將新加的元素也放到第10個位置,將第10個位置的原有Entry賦值給當前新加的 Entry的next屬性。陣列儲存的是連結串列,連結串列是為了解決雜湊衝突的,這一點要注意。


幾個關鍵的屬性
儲存資料的陣列
transient Entry[] table; 這個上面已經講到了
預設容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

預設載入因子,載入因子是一個比例,當HashMap的資料大小>=容量*載入因子時,HashMap會將容量擴容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
當實際資料大小超過threshold時,HashMap會將容量擴容,threshold=容量*載入因子
int threshold;
載入因子
final float loadFactor;


HashMap的初始過程
建構函式1

Java程式碼 複製程式碼 收藏程式碼
  1. public HashMap(int initialCapacity, float loadFactor) {   
  2. if (initialCapacity < 
    0)   
  3. thrownew IllegalArgumentException("Illegal initial capacity: " +   
  4.                                            initialCapacity);   
  5. if (initialCapacity > MAXIMUM_CAPACITY)   
  6.         initialCapacity = MAXIMUM_CAPACITY;   
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))   
  8. thrownew IllegalArgumentException("Illegal load factor: " +   
  9.                                            loadFactor);   
  10. // Find a power of 2 >= initialCapacity
  11. int capacity = 1;   
  12. while (capacity < initialCapacity)   
  13.         capacity <<= 1;   
  14. this.loadFactor = loadFactor;   
  15.     threshold = (int)(capacity * loadFactor);   
  16.     table = new Entry[capacity];   
  17.     init();   
  18. }  
Java程式碼
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.     if (initialCapacity < 0)  
  3.         thrownew IllegalArgumentException("Illegal initial capacity: " +  
  4.                                            initialCapacity);  
  5.     if (initialCapacity > MAXIMUM_CAPACITY)  
  6.         initialCapacity = MAXIMUM_CAPACITY;  
  7.     if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.         thrownew IllegalArgumentException("Illegal load factor: " +  
  9.                                            loadFactor);  
  10.     // Find a power of 2 >= initialCapacity
  11.     int capacity = 1;  
  12.     while (capacity < initialCapacity)  
  13.         capacity <<= 1;  
  14.     this.loadFactor = loadFactor;  
  15.     threshold = (int)(capacity * loadFactor);  
  16.     table = new Entry[capacity];  
  17.     init();  
  18. }  



重點注意這裡

Java程式碼 複製程式碼 收藏程式碼
  1. while (capacity < initialCapacity)   
  2.             capacity <<= 1;  
Java程式碼
  1. while (capacity < initialCapacity)  
  2.             capacity <<= 1;  


capacity才是初始容量,而不是initialCapacity,這個要特別注意,如果執行new HashMap(9,0.75);那麼HashMap的初始容量是16,而不是9,想想為什麼吧。

建構函式2

Java程式碼 複製程式碼 收藏程式碼
  1. public HashMap(int initialCapacity) {   
  2. this(initialCapacity, DEFAULT_LOAD_FACTOR);   
  3.     }  
Java程式碼
  1. public HashMap(int initialCapacity) {  
  2.         this(initialCapacity, DEFAULT_LOAD_FACTOR);  
  3.     }  


建構函式3,全部都是預設值

Java程式碼 複製程式碼 收藏程式碼
  1. public HashMap() {   
  2. this.loadFactor = DEFAULT_LOAD_FACTOR;   
  3.      threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);   
  4.      table = new Entry[DEFAULT_INITIAL_CAPACITY];   
  5.      init();   
  6.  }  
Java程式碼
  1. public HashMap() {  
  2.      this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.      threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  4.      table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.      init();  
  6.  }  


建構函式4

Java程式碼 複製程式碼 收藏程式碼
  1. public HashMap(Map<? extends K, ? extends V> m) {   
  2. this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,   
  3.                     DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);   
  4.       putAllForCreate(m);   
  5.   }  
Java程式碼
  1. public HashMap(Map<? extends K, ? extends V> m) {  
  2.       this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  
  3.                     DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
  4.       putAllForCreate(m);  
  5.   }  



如何雜湊
        HashMap並不是直接將物件的hashcode作為雜湊值的,而是要把key的hashcode作一些運算以得到最終的雜湊值,並且得到的雜湊值也不是在陣列中的位置哦,無論是get還是put還是別的方法,計算雜湊值都是這一句:
int hash = hash(key.hashCode());
hash函式如下:

Java程式碼 複製程式碼 收藏程式碼
  1. staticint hash(int h) {   
  2. return useNewHash ? newHash(h) : oldHash(h);   
  3.   }  
Java程式碼
  1. staticint hash(int h) {  
  2.   return useNewHash ? newHash(h) : oldHash(h);  
  3.   }  


useNewHash宣告如下:
 

Java程式碼 複製程式碼 收藏程式碼
  1. privatestaticfinalboolean useNewHash;   
  2. static { useNewHash = false; }  
Java程式碼
  1. privatestaticfinalboolean useNewHash;  
  2.    static { useNewHash = false; }  

這說明useNewHash其實一直為false且不可改變的,hash函式裡對 useNewHash的判斷真是多餘的。

Java程式碼 複製程式碼 收藏程式碼
  1. privatestaticint oldHash(int h) {   
  2.     h += ~(h << 9);   
  3.     h ^=  (h >>> 14);   
  4.     h +=  (h << 4);   
  5.     h ^=  (h >>> 10);   
  6. return h;   
  7. }   
  8. privatestaticint newHash(int h) {   
  9. // This function ensures that hashCodes that differ only by
  10. // constant multiples at each bit position have a bounded
  11. // number of collisions (approximately 8 at default load factor).
  12.     h ^= (h >>> 20) ^ (h >>> 12);   
  13. return h ^ (h >>> 7) ^ (h >>> 4);   
  14. }  
Java程式碼
  1. privatestaticint oldHash(int h) {  
  2.     h += ~(h << 9);  
  3.     h ^=  (h >>> 14);  
  4.     h +=  (h << 4);  
  5.     h ^=  (h >>> 10);  
  6.     return h;  
  7. }  
  8. privatestaticint newHash(int h) {  
  9.     // This function ensures that hashCodes that differ only by
  10.     // constant multiples at each bit position have a bounded
  11.     // number of collisions (approximately 8 at default load factor).
  12.     h ^= (h >>> 20) ^ (h >>> 12);  
  13.     return h ^ (h >>> 7) ^ (h >>> 4);  
  14. }  



其實HashMap的雜湊函式會一直都是oldHash。


如果確定資料的位置
看下面兩行
 

Java程式碼 複製程式碼 收藏程式碼
  1. int hash = hash(k.hashCode());   
  2. int i = indexFor(hash, table.length);  
Java程式碼
  1. int hash = hash(k.hashCode());  
  2.   int i = indexFor(hash, table.length);  

第一行,上面講過了,是得到雜湊值,第二行,則是根據雜湊指計算元素在陣列中的位置了,位置的計算是將雜湊值和陣列長度按位與運算。

Java程式碼 複製程式碼 收藏程式碼
  1. staticint indexFor(int h, int length) {   
  2. return h & (length-1);   
  3.  }  
Java程式碼
  1. staticint indexFor(int h, int length) {  
  2.      return h & (length-1);  
  3.  }  

“h & (length-1)”其實這裡是很有講究的,為什麼是和(length-1)進行按位與運算呢?這樣做是為了提高HashMap的效率。什麼?這樣能提高效率?且聽我細細道來。

首先我們要確定一下,HashMap的陣列長度永遠都是偶數,即使你在初始化的時候是這樣的new HashMap(15,0.75);因為在建構函式內部,上面也講過,有這樣的一段程式碼:

Java程式碼 複製程式碼 收藏程式碼
  1. while (capacity < initialCapacity)   
  2.             capacity <<= 1;  
Java程式碼
  1. while (capacity < initialCapacity)  
  2.             capacity <<= 1;  

所以length-1一定是個奇數,假設現在長度為16,減去1後就是15,對應的二進位制是:1111。

假設有兩個元素,一個雜湊值是8,二進位制是1000,一個雜湊值是9,二進位制是1001。和1111與運算後,分別還是1000和1001,它們被分配在了陣列的不同位置,這樣,雜湊的分佈非常均勻。

那麼,如果陣列長度是奇數,減去1後就是偶數了,偶數對應的二進位制最低位一定是0了,例如14二進位制1110。對上面兩個數子分別與運算,得到1000和1000。看到了嗎?都是一樣的值,雜湊值8和9的元素多被儲存在陣列同一個位置的連結串列中。在操作的時候,連結串列中的元素越多,效率越低,因為要不停的對連結串列迴圈比較。所以,一定要雜湊均勻分佈,儘量減少雜湊衝突,減少了雜湊衝突,就減少了連結串列迴圈,就提高了效率。


put方法到底作了什麼?

Java程式碼 複製程式碼 收藏程式碼
  1. public V put(K key, V value) {   
  2. if (key == null)   
  3. return putForNullKey(value);   
  4. int hash = hash(key.hashCode());   
  5. int i = indexFor(hash, table.length);   
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  7.          Object k;   
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  9.              V oldValue = e.value;   
  10.              e.value = value;   
  11.              e.recordAccess(this);   
  12. return oldValue;   
  13.          }   
  14.      }   
  15.      modCount++;   
  16.      addEntry(hash, key, value, i);   
  17. returnnull;   
  18.  }  
Java程式碼
  1. public V put(K key, V value) {  
  2.  if (key == null)  
  3.      return putForNullKey(value);  
  4.      int hash = hash(key.hashCode());  
  5.      int i = indexFor(hash, table.length);  
  6.      for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.          Object k;  
  8.          if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.              V oldValue = e.value;  
  10.              e.value = value;  
  11.              e.recordAccess(this);  
  12.              return oldValue;  
  13.          }  
  14.      }  
  15.      modCount++;  
  16.      addEntry(hash, key, value, i);  
  17.      returnnull;  
  18.  }  


如果key為NULL,則是單獨處理的,看看putForNullKey方法:
 

Java程式碼 複製程式碼 收藏程式碼
  1. private V putForNullKey(V value) {   
  2. int hash = hash(NULL_KEY.hashCode());   
  3. int i = indexFor(hash, table.length);   
  4. for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  5. if (e.key == NULL_KEY) {   
  6.               V oldValue = e.value;   
  7.               e.value = value;   
  8.               e.recordAccess(this);   
  9. return oldValue;   
  10.           }   
  11.       }   
  12.       modCount++;   
  13.       addEntry(hash, (K) NULL_KEY, value, i);   
  14. returnnull;   
  15.   }  
Java程式碼
  1. private V putForNullKey(V value) {  
  2.       int hash = hash(NULL_KEY.hashCode());  
  3.       int i = indexFor(hash, table.length);  
  4.       for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  5.           if (e.key == NULL_KEY) {  
  6.               V oldValue = e.value;  
  7.               e.value = value;  
  8.               e.recordAccess(this);  
  9.               return oldValue;  
  10.           }  
  11.       }  
  12.       modCount++;  
  13.       addEntry(hash, (K) NULL_KEY, value, i);  
  14.       returnnull;  
  15.   }  

NULL_KEY的宣告:static final Object NULL_KEY = new Object();
這一段程式碼是處理雜湊衝突的,就是說,在陣列某個位置的物件可能並不是唯一的,它是一個連結串列結構,根據雜湊值找到連結串列後,還要對連結串列遍歷,找出key相等的物件,替換它,並且返回舊的值。

Java程式碼 複製程式碼 收藏程式碼
  1. for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  2. if (e.key == NULL_KEY) {   
  3.              V oldValue = e.value;   
  4.              e.value = value;   
  5.              e.recordAccess(this);   
  6. return oldValue;   
  7.          }   
  8.      }  
Java程式碼
  1. for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  2.          if (e.key == NULL_KEY) {  
  3.              V oldValue = e.value;  
  4.              e.value = value;  
  5.              e.recordAccess(this);  
  6.              return oldValue;  
  7.          }  
  8.      }  

如果遍歷完了該位置的連結串列都沒有找到有key相等的,那麼將當前物件增加到連結串列裡面去

Java程式碼 複製程式碼 收藏程式碼
  1. modCount++;   
  2. addEntry(hash, (K) NULL_KEY, value, i);   
  3. returnnull;  
Java程式碼
  1. modCount++;  
  2. addEntry(hash, (K) NULL_KEY, value, i);  
  3. returnnull;  

且看看addEntry方法

Java程式碼 複製程式碼 收藏程式碼
  1. void addEntry(int hash, K key, V value, int bucketIndex) {   
  2. Entry<K,V> e = table[bucketIndex];   
  3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   
  4. if (size++ >= threshold)   
  5.         resize(2 * table.length);   
  6. }  
Java程式碼
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2. Entry<K,V> e = table[bucketIndex];  
  3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.     if (size++ >= threshold)  
  5.         resize(2 * table.length);  
  6. }  

table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一個Entry物件,並放在當前位置的Entry連結串列的頭部,看看下面的 Entry建構函式就知道了,注意紅色部分。

Java程式碼 複製程式碼 收藏程式碼
  1. Entry(int h, K k, V v, Entry<K,V> n) {   
  2.        value = v;   
  3.        next = n;   
  4.        key = k;   
  5.        hash = h;   
  6.    }  
Java程式碼
  1. Entry(int h, K k, V v, Entry<K,V> n) {  
  2.        value = v;  
  3.        next = n;  
  4.        key = k;  
  5.        hash = h;  
  6.    }  

如何擴容?
        當put一個元素時,如果達到了容量限制,HashMap就會擴容,新的容量永遠是原來的2倍。
上面的put方法裡有這樣的一段:

Java程式碼 複製程式碼 收藏程式碼
  1. if (size++ >= threshold)   
  2.             resize(2 * table.length);  
Java程式碼
  1. if (size++ >= threshold)  
  2.             resize(2 * table.length);  

這是擴容判斷,要注意,並不是資料尺寸達到HashMap的最大容量時才擴容,而是達到 threshold指定的值時就開始擴容, threshold=最大容量*載入因子。 看看resize方法

Java程式碼 複製程式碼 收藏程式碼
  1. void resize(int newCapacity) {   
  2.       Entry[] oldTable = table;   
  3. int oldCapacity = oldTable.length;   
  4. if (oldCapacity == MAXIMUM_CAPACITY) {   
  5.           threshold = Integer.MAX_VALUE;   
  6. return;   
  7.       }   
  8.       Entry[] newTable = new Entry[newCapacity];   
  9.       transfer(newTable);   
  10.       table = newTable;   
  11.       threshold = (int)(newCapacity * loadFactor);   
  12.   }  
Java程式碼
  1. void resize(int newCapacity) {  
  2.       Entry[] oldTable = table;  
  3.       int oldCapacity = oldTable.length;  
  4.       if (oldCapacity == MAXIMUM_CAPACITY) {  
  5.           threshold = Integer.MAX_VALUE;  
  6.           return;  
  7.       }  
  8.       Entry[] newTable = new Entry[newCapacity];  
  9.       transfer(newTable);  
  10.       table = newTable;  
  11.       threshold = (int)(newCapacity * loadFactor);  
  12.   }  

重點看看紅色部分的 transfer方法
 

Java程式碼 複製程式碼 收藏程式碼
  1. void transfer(Entry[] newTable) {   
  2.       Entry[] src = table;   
  3. int newCapacity = newTable.length;   
  4. for (int j = 0; j < src.length; j++) {   
  5.           Entry<K,V> e = src[j];   
  6. if (e != null) {   
  7.               src[j] = null;   
  8. do {   
  9.                   Entry<K,V> next = e.next;   
  10. int i = indexFor(e.hash, newCapacity);   
  11.                   e.next = newTable[i];   
  12.                   newTable[i] = e;