標籤:
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 < 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); // 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;
capacity才是初始容量,而不是initialCapacity,這個要特別注意,如果執行new HashMap(9,0.75);那麼HashMap的初始容量是16,而不是9,想想為什麼吧。
建構函式2
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();}
建構函式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);}
如何雜湊
HashMap並不是直接將對象的hashcode作為雜湊值的,而是要把key的hashcode作一些運算以得到最終的雜湊值,並且得到的雜湊值也不是在數組中的位置哦,無論是get還是put還是別的方法,計算雜湊值都是這一句:
int hash = hash(key.hashCode());
hash函數如下:
static int hash(int h) { return useNewHash ? newHash(h) : oldHash(h);}
useNewHash聲明如下:
private static final boolean useNewHash;static { useNewHash = false; }
這說明useNewHash其實一直為false且不可改變的,hash函數裡對 useNewHash的判斷真是多餘的。
private static int oldHash(int h) { h += ~(h << 9); h ^= (h >>> 14); h += (h << 4); h ^= (h >>> 10); return h;}private static int 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);
第一行,上面講過了,是得到雜湊值,第二行,則是根據雜湊指計算元素在數組中的位置了,位置的計算是將雜湊值和數組長度按位與運算。
static int indexFor(int h, int length) { return h & (length-1);}
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); return null;}
如果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); return null;}
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; }}
如果遍曆完了該位置的鏈表都沒有找到有key相等的,那麼將當前對象增加到鏈表裡面去
modCount++;addEntry(hash, (K) NULL_KEY, value, i);return null;
且看看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); } 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;}
如何擴容?
當put一個元素時,如果達到了容量限制,HashMap就會擴容,新的容量永遠是原來的2倍。
上面的put方法裡有這樣的一段:
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);}
重點看看紅色部分的 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; e = next; } while (e != null); } }}
tranfer方法將所有的元素重新雜湊,因為新的容量變大,所以每個元素的雜湊值和位置都是不一樣的。
正確的使用HashMap
1:不要在並發情境中使用HashMap
HashMap是線程不安全的,如果被多個線程共用的操作,將會引發不可預知的問題,據sun的說法,在擴容時,會引起鏈表的閉環,在get元素時,就會無限迴圈,後果是cpu 100%。
看看get方法的紅色部分
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null;}
2:如果資料大小是固定的,那麼最好給HashMap設定一個合理的容量值
根據上面的分析,HashMap的初始預設容量是16,預設載入因子是0.75,也就是說,如果採用HashMap的預設建構函式,當增加資料時,資料實際容量超過16*0.75=12時,HashMap就擴容,擴容帶來一系列的運算,建立一個是原來容量2倍的數組,對原有元素全部重新雜湊,如果你的資料有幾千幾萬個,而用預設的HashMap建構函式,那結果是非常悲劇的,因為HashMap不斷擴容,不斷雜湊,在使用HashMap的情境裡,不會是多個線程共用一個HashMap,除非對HashMap封裝並同步,由此產生的記憶體開銷和cpu開銷在某些情況下可能是致命的。
轉自:http://www.java3z.com/cwbwebhome/article/article8/81388.html?id=3973
HashMap結構及使用