默認初始大小,值爲16,要求必須爲2的冪
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最大容量,必須不大於2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
默認加載因子,值爲0.7513
static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap空數組
static final Entry<?,?>[] EMPTY_TABLE = {};
可選的默認哈希閾值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;複製代碼
注意:java
jdk1.7中HashMap默認採用數組+單鏈表方式存儲元素,當元素出現哈希衝突時,會存儲到該位置的單鏈表中;算法
jdk1.8除了數組和單鏈表外,當單鏈表中元素個數超過8個時,會進而轉化爲紅黑樹存儲,巧妙地將遍歷元素時時間複雜度從O(n)下降到了O(logn))。數組
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}複製代碼
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;//和jdk8不一樣,初始閾值就是初始容量,並沒作2次冪處理
init();
}複製代碼
三、帶參構造函數,指定Map集合:bash
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
if (table == EMPTY_TABLE) {
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}複製代碼
說明:執行構造函數時,存儲元素的數組並不會進行初始化,而是在第一次放入元素的時候,纔會進行初始化操做。建立HashMap對象時,僅僅計算初始容量和新增閾值。併發
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//初始化數組
}
if (key == null)//key爲null,作key爲null的添加
return putForNullKey(value);
int hash = hash(key);//計算鍵值的哈希
int i = indexFor(hash, table.length);//根據哈希值獲取在數組中的索引位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍歷索引位置的單鏈表,判斷是否存在指定key
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//key已存在則更新value值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);//key不存在,則插入元素
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已存在,更新value值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//不存在則新增,key爲null的哈希值爲035 return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//插入位置存在元素,而且元素個數大於等於新增閾值
resize(2 * table.length);//進行2倍擴容
hash = (null != key) ? hash(key) : 0;//擴容中可能會調整哈希種子的值,因此從新計算哈希值
bucketIndex = indexFor(hash, table.length);//從新計算在擴容後數組中的位置
}
createEntry(hash, key, value, bucketIndex);//添加元素
}
//計算對象哈希值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {//String採用單獨的算法
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();//利用哈希種子異或哈希值,爲了進行優化,增長隨機性
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);//這裏的移位異或操做屬於擾亂函數,都是爲了增長哈希值的隨機性,下降哈希衝突的機率
}
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++;
}複製代碼
根據指定的大小,初始化數組
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//根據容量和加載因子計算閾值,最大爲2^30+1
table = new Entry[capacity];//建立指定容量大小的數組
initHashSeedAsNeeded(capacity);
}
//獲取大於指定值的最小2次冪,最大爲2^30
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;
}複製代碼
哈希種子,是爲了優化哈希函數,讓其值更加隨機,從而下降哈希衝突的機率。經過HashMap中私有靜態類Holder,在JVM啓動的時候,指定-Djdk.map.althashing.threshold=值,來設置可選的哈希閾值,從而在initHashSeedAsNeeded中決定是否須要調整哈希種子。dom
private static class Holder {
/** * Table capacity above which to switch to use alternative hashing. */
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));//經過-Djdk.map.althashing.threshold=值指定可選哈希閾值
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;//默認爲Integer.MAX_VALUE
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;//指定可選的哈希閾值,在initHashSeedAsNeeded做爲是否初始化哈希種子的斷定條件
}
}
//根據容量決定是否須要初始化哈希種子
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;//哈希種子默認爲0
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//若是容量大於可選的哈希閾值,則須要初始化哈希種子
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)//生成一個隨機的哈希種子
: 0;
}
return switching;
}複製代碼
//按照指定容量進行數組擴容
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);//按照擴容後容量從新計算閾值
}
//將元素從新分配到新數組中
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;
}
}
}複製代碼
上述擴容代碼,在併發狀況下執行,就會出現常說的鏈表成環的問題,下面經過示例來分析:
2.一、初始狀態:
函數
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;//線程1執行到此行代碼,e爲10,next爲2。此時CPU調度線程2執行。
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;
}
}複製代碼
2.三、 線程2執行:
線程2此時獲取到CPU執行權,執行transfer()中代碼:優化
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;
}
}複製代碼
第一次遍歷:e爲10,next爲2,rehash爲false,i爲2,newTable[2]爲null,10.next爲null,newTable[2]爲10,e爲2。
第二次遍歷:e爲2,next爲null,rehash爲false,i爲2,newTable[2]爲10,2.next爲10,newTable[2]爲2,e爲null。
第三次遍歷:e爲null,退出循環。
注意,此時原table中元素2的next指向了10。
ui
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;//線程1執行到此行代碼,e爲10,next爲2。CPU調度線程1繼續執行。
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;
}
}複製代碼
當前:e爲10,next爲2,rehash爲false,i爲2,newTable[2]爲null,修改:10.next爲null,newTable[2]爲10,e爲2。
第二次遍歷:當前:e爲2,next爲10【線程2執行後的結果】,rehash爲false,i爲2,newTable[2]爲10,修改:2.next爲10,newTable[2]爲2,e爲10。
第三次遍歷:當前:e爲10,next爲null,rehash爲false,i爲2,newTable[2]爲2,修改:10.next爲2,newTable[2]爲10,e爲null,退出循環。
此時,鏈表成環,若是進行查找,會陷入死循環!!!
this
由上例可知,HashMap在jdk1.7中採用頭插入法,在擴容時會改變鏈表中元素本來的順序,以致於在併發場景下致使鏈表成環的問題。而在jdk1.8中採用尾插入法,在擴容時會保持鏈表元素本來的順序,就不會出現鏈表成環的問題了。
經過上述的分析,在這裏總結下HashMap在1.7和1.8之間的變化: