j2ee(9) HashMap源碼
阿新 • • 發佈:2017-07-13
|| 為什麽 引用 1.7 lin length 機會 lar number
系統環境: JDK1.7
HashMap的基本結構:數組 + 鏈表。主數組不存儲實際的數據,存儲的是鏈表首地址。
成員變量
//默認數組的初始化大小為16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大數組大小 static final int MAXIMUM_CAPACITY = 1 << 30; //默認負載因子,默認0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //空的數組 static final Entry<?,?>[] EMPTY_TABLE = {};//存儲元素的實體數組 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //HashMap中元素的個數 transient int size; //臨界值,threshold = 負載因子 * 當前數組容量,實際個數超過臨界值時,會進行擴容 int threshold; //負載因子 final float loadFactor; transient int modCount; static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
Entry是HashMap中的一個靜態內部類
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; //存儲指向下一個Entry的引用,單鏈表結構 int hash; //對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重復計算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ....... }
構造方法
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); this.loadFactor = loadFactor; threshold = initialCapacity; init(); //init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現 } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); putAllForCreate(m); } 前三個常規構造器中,沒有為數組table分配實際的內存空間,只進行了賦值操作。對於空的HashMap只有在執行put操作的時候才真正構建table數組。 而第4個構造器,則會為數組table分配實際的內存空間。關註最後一個構造方法,跟進inflateTable() private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); //capacity一定是2的次冪 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //為主幹數組table在內存中分配存儲空間 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); } //通過roundUpToPowerOf2(toSize)可以確保capacity為大於或等於toSize的最接近toSize的二次冪 //比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32., private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } 疑問:capacity為什麽是2的N次方? 一會兒在解釋。
put方法分析
public V put(K key, V value) { // 若為第一次put,則先初始化數組 if (table == EMPTY_TABLE) { inflateTable(threshold); } // key為null,放在table[0]即數組第一個的位置 if (key == null) return putForNullKey(value); // 根據key計算hash值,具體計算hash的算法我不太懂 int hash = hash(key); // 根據hash值和表的長度,確定這個元素存放在數組的第幾個位置,即求得元素在數組中的位置的索引值 int i = indexFor(hash, table.length); // 遍歷該位置的鏈表,如果有重復的key,則將value覆蓋 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; } } // 修改次數+1 modCount++; // 將新加入的數據掛載到table[i]的位置 addEntry(hash, key, value, i); return null; } private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { //如果有key為null的對象存在,則覆蓋掉 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); //如果鍵為null的話,則hash值為0 return null; } //根據hashCode和數組的長度,返回元素存儲的索引位置 static int indexFor(int h, int length) { return h & (length-1); } 這塊就能印證之前數組長度為什麽要為2的N次方了. 首先,若數組長度為2的N次方,則length必然為偶數,則length-1必然為奇數,在2進制的表示中奇數的最後一位為1,所以與奇數做“&”操作,最後的結果可能為奇數,也可能為偶數。 其次,若length為奇數,則length-1為偶數,偶數在2進制中最後一位為0,那麽與偶數做“&”操作,最後的結果只可能是偶數,不可能為奇數,所以在奇數位置的空間不會存儲到元素,這樣會有二分之一的空間被浪費掉。 綜上所述,數組長度取2的N次方,目的是為了能讓元素均勻的分布在數組中,減小發生沖突的機會。
//與已存在的鏈表的key不重復的話,則新增節點 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // 判斷數組是否需要擴容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } //新增加的Entry 會添加到鏈表的頂端 即table[bucketIndex]上 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } //再來看看需要擴容的情況,當現有的元素個數大於等於臨界值的時候需要進行擴容,跟進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, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //細細體會每個Entry的遷移過程 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
其他方法相對簡單 就不整理了。
整理自《http://blog.csdn.net/zw0283/article/details/51177547》
j2ee(9) HashMap源碼