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
- public HashMap(int initialCapacity, float loadFactor) {
- if (initialCapacity <
- thrownew IllegalArgumentException("Illegal initial capacity: " +
- initialCapacity);
- if (initialCapacity > MAXIMUM_CAPACITY)
- initialCapacity = MAXIMUM_CAPACITY;
- if (loadFactor <= 0 || Float.isNaN(loadFactor))
- thrownew IllegalArgumentException("Illegal load factor: " +
- loadFactor);
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- this.loadFactor = loadFactor;
- threshold = (int)(capacity * loadFactor);
- table = new Entry[capacity];
- init();
- }
- public HashMap(int initialCapacity, float loadFactor) {
- if (initialCapacity < 0)
- thrownew IllegalArgumentException("Illegal initial capacity: " +
- initialCapacity);
- if (initialCapacity > MAXIMUM_CAPACITY)
- initialCapacity = MAXIMUM_CAPACITY;
- if (loadFactor <= 0 || Float.isNaN(loadFactor))
- thrownew IllegalArgumentException("Illegal load factor: " +
- loadFactor);
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- this.loadFactor = loadFactor;
- threshold = (int)(capacity * loadFactor);
- table = new Entry[capacity];
- init();
- }
重點注意這裡
- while (capacity < initialCapacity)
- capacity <<= 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
capacity才是初始容量,而不是initialCapacity,這個要特別注意,如果執行new HashMap(9,0.75);那麼HashMap的初始容量是16,而不是9,想想為什麼吧。
建構函式2
- public HashMap(int initialCapacity) {
- this(initialCapacity, DEFAULT_LOAD_FACTOR);
- }
- public HashMap(int initialCapacity) {
- this(initialCapacity, DEFAULT_LOAD_FACTOR);
- }
建構函式3,全部都是預設值
- public HashMap() {
- this.loadFactor = DEFAULT_LOAD_FACTOR;
- threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
- table = new Entry[DEFAULT_INITIAL_CAPACITY];
- init();
- }
- public HashMap() {
- this.loadFactor = DEFAULT_LOAD_FACTOR;
- threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
- table = new Entry[DEFAULT_INITIAL_CAPACITY];
- init();
- }
建構函式4
- public HashMap(Map<? extends K, ? extends V> m) {
- this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
- DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
- putAllForCreate(m);
- }
- public HashMap(Map<? extends K, ? extends V> m) {
- this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
- DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
- putAllForCreate(m);
- }
如何雜湊
HashMap並不是直接將物件的hashcode作為雜湊值的,而是要把key的hashcode作一些運算以得到最終的雜湊值,並且得到的雜湊值也不是在陣列中的位置哦,無論是get還是put還是別的方法,計算雜湊值都是這一句:
int hash = hash(key.hashCode());
hash函式如下:
- staticint hash(int h) {
- return useNewHash ? newHash(h) : oldHash(h);
- }
- staticint hash(int h) {
- return useNewHash ? newHash(h) : oldHash(h);
- }
useNewHash宣告如下:
- privatestaticfinalboolean useNewHash;
- static { useNewHash = false; }
- privatestaticfinalboolean useNewHash;
- static { useNewHash = false; }
這說明useNewHash其實一直為false且不可改變的,hash函式裡對 useNewHash的判斷真是多餘的。
- privatestaticint oldHash(int h) {
- h += ~(h << 9);
- h ^= (h >>> 14);
- h += (h << 4);
- h ^= (h >>> 10);
- return h;
- }
- privatestaticint newHash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
- privatestaticint oldHash(int h) {
- h += ~(h << 9);
- h ^= (h >>> 14);
- h += (h << 4);
- h ^= (h >>> 10);
- return h;
- }
- privatestaticint newHash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
其實HashMap的雜湊函式會一直都是oldHash。
如果確定資料的位置
看下面兩行
- int hash = hash(k.hashCode());
- int i = indexFor(hash, table.length);
- int hash = hash(k.hashCode());
- int i = indexFor(hash, table.length);
第一行,上面講過了,是得到雜湊值,第二行,則是根據雜湊指計算元素在陣列中的位置了,位置的計算是將雜湊值和陣列長度按位與運算。
- staticint indexFor(int h, int length) {
- return h & (length-1);
- }
- staticint indexFor(int h, int length) {
- return h & (length-1);
- }
“h & (length-1)”其實這裡是很有講究的,為什麼是和(length-1)進行按位與運算呢?這樣做是為了提高HashMap的效率。什麼?這樣能提高效率?且聽我細細道來。
首先我們要確定一下,HashMap的陣列長度永遠都是偶數,即使你在初始化的時候是這樣的new HashMap(15,0.75);因為在建構函式內部,上面也講過,有這樣的一段程式碼:
Java程式碼- while (capacity < initialCapacity)
- capacity <<= 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
所以length-1一定是個奇數,假設現在長度為16,減去1後就是15,對應的二進位制是:1111。
假設有兩個元素,一個雜湊值是8,二進位制是1000,一個雜湊值是9,二進位制是1001。和1111與運算後,分別還是1000和1001,它們被分配在了陣列的不同位置,這樣,雜湊的分佈非常均勻。
那麼,如果陣列長度是奇數,減去1後就是偶數了,偶數對應的二進位制最低位一定是0了,例如14二進位制1110。對上面兩個數子分別與運算,得到1000和1000。看到了嗎?都是一樣的值,雜湊值8和9的元素多被儲存在陣列同一個位置的連結串列中。在操作的時候,連結串列中的元素越多,效率越低,因為要不停的對連結串列迴圈比較。所以,一定要雜湊均勻分佈,儘量減少雜湊衝突,減少了雜湊衝突,就減少了連結串列迴圈,就提高了效率。
put方法到底作了什麼?
- 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);
- returnnull;
- }
- 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);
- returnnull;
- }
如果key為NULL,則是單獨處理的,看看putForNullKey方法:
- private V putForNullKey(V value) {
- int hash = hash(NULL_KEY.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- if (e.key == NULL_KEY) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, (K) NULL_KEY, value, i);
- returnnull;
- }
- private V putForNullKey(V value) {
- int hash = hash(NULL_KEY.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- if (e.key == NULL_KEY) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, (K) NULL_KEY, value, i);
- returnnull;
- }
NULL_KEY的宣告:static final Object NULL_KEY = new Object();
這一段程式碼是處理雜湊衝突的,就是說,在陣列某個位置的物件可能並不是唯一的,它是一個連結串列結構,根據雜湊值找到連結串列後,還要對連結串列遍歷,找出key相等的物件,替換它,並且返回舊的值。
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- if (e.key == NULL_KEY) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- if (e.key == NULL_KEY) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
如果遍歷完了該位置的連結串列都沒有找到有key相等的,那麼將當前物件增加到連結串列裡面去
- modCount++;
- addEntry(hash, (K) NULL_KEY, value, i);
- returnnull;
- modCount++;
- addEntry(hash, (K) NULL_KEY, value, i);
- returnnull;
且看看addEntry方法
- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
- }
- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
- }
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一個Entry物件,並放在當前位置的Entry連結串列的頭部,看看下面的 Entry建構函式就知道了,注意紅色部分。
- Entry(int h, K k, V v, Entry<K,V> n) {
- value = v;
- next = n;
- key = k;
- hash = h;
- }
- Entry(int h, K k, V v, Entry<K,V> n) {
- value = v;
- next = n;
- key = k;
- hash = h;
- }
如何擴容?
當put一個元素時,如果達到了容量限制,HashMap就會擴容,新的容量永遠是原來的2倍。
上面的put方法裡有這樣的一段:
- if (size++ >= threshold)
- resize(2 * table.length);
- if (size++ >= threshold)
- resize(2 * table.length);
這是擴容判斷,要注意,並不是資料尺寸達到HashMap的最大容量時才擴容,而是達到 threshold指定的值時就開始擴容, threshold=最大容量*載入因子。 看看resize方法
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
- }
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
- }
重點看看紅色部分的 transfer方法
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- for (int j = 0; j < src.length; j++) {
- Entry<K,V> e = src[j];
- if (e != null) {
- src[j] = null;
- do {
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;