HashMap在JDK1.7版本中的數據存儲結構實際上是一個 Entry<?, ?>[] EMPTY_TABLE數組
static final Entry<?, ?>[] EMPTY_TABLE = {};
// table就是HashMap實際存儲數組的地方
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
顯而易見,其中的每個元素又是一個鏈表
static class Entry<K, V> implements Map.Entry<K, V> {
final K key;
V value;
Entry<K, V> next;
int hash;
...//省略後續代碼
因此,Java7 HashMap的結構大致如下圖
總結:簡單來說,HashMap中的數據存儲結構是個數組,而每個元素都是一個單向鏈表,鏈表中每個元素是一個Entry的內部類對象,每個對象包含四個屬性:key,value,hash值,和用於單向鏈表的next
重要的成員變量:看一下其中的其他成員變量
// 默認的HashMap的空間大小16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認值是16
// hashMap最大的空間大小
static final int MAXIMUM_CAPACITY = 1 << 30;
// HashMap默認負載因子,負載因子越小,hash衝突機率越低
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//初始化的空數組
static final Entry<?, ?>[] EMPTY_TABLE = {};
// table就是HashMap實際存儲數組的地方
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
// HashMap 實際存儲的元素個數
transient int size;
// 臨界值(超過這個值則開始擴容),公式爲(threshold = capacity * loadFactor)
int threshold;
// HashMap 負載因子
final float loadFactor;
構造方法:從源碼中可以看出HashMap一共有個四個構造,他們分別爲
//1.默認構造,會調用默認默認空間大小16和默認負載因子0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//2.指定大小但不指定負載因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//3.指定大小和負載因子
public HashMap(int initialCapacity, float loadFactor) {
//此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(2的30次方)
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方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
init();
}
//4.使用默認構造創建對象並將指定的map集合放入新創建的對象中
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);
}
上面四個構造實際上都是在使用第三個構造方法:類中有幾個比較重要的字段:
//實際存儲的key-value鍵值對的個數
transient int size;
//當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍。
int capacity
//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,
//threshold一般爲 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,後面會詳細談到
int threshold;
//負載因子,代表了table的填充度有多少,默認是0.75
final float loadFactor;
//用於快速失敗,由於HashMap非線程安全,在對HashMap進行迭代時,
//如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;
從源碼中不難看出,實際上,在構造器中(第四個除外),並沒有爲數組分配內存空間,而是在put操作的時候才進行數據的構建
put操作
下面看下put方法的執行過程
- 首先要判斷數字是否爲空數組,如果是空數組的話,需要對數組進行初始化
- 如果key是null的話,回家元素的值彷彿唉table的0索引上,此時終止操作
- 如果key值不是null的話
- 對key進行hash操作,獲取到hash值
- 找到key對應的數組下標
- 獲取到鏈表對象後遍歷鏈表,看是否有重複的key存在,如果有直接覆蓋並返回原來位置上的值,就此結束
- 如果不存在重複的key,將此該key和value組裝程Entry對象添加到鏈表中(存在數組擴容問題-後面有介紹)
//put操作源碼
public V put(K key, V value) {
// 當插入第一個元素的時候,需要先初始化數組大小
if (table == EMPTY_TABLE) {
// 數組初始化
inflateTable(threshold);
}
// 如果 key 爲 null,最終會將這個 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到對應的數組下標
int i = indexFor(hash, table.length);
// 3. 遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,如果有,直接覆蓋,put 方法返回舊值就結束了
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))) { // key -> value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重複的 key,將此 entry 添加到鏈表中
addEntry(hash, key, value, i);
return null;
}
數組初始化
ps:在添加元素的開始,需要對數組是否初始化做判斷,如果沒有初始化需要做初始化處理
保證數組大小是是2的N次方的好處:
當數組長度爲2的n次冪的時候, 1、位移運算效率較高 2、不同的key的hash計算結果相同的機率較低,減少hash碰撞,使得數據在數組上分佈的比較均勻, 查詢的時候就不用遍歷某個位置上的鏈表,可以提升定位元素的的效率
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 保證數組大小一定是 2 的 n 次方。
// new HashMap(519),大小是1024
//將數組大小保持爲2的n次方,在Java7和Java8的HashMap和 ConcurrentHashMap 都有相應的要求,實現代碼略有不同
int capacity = roundUpToPowerOf2(toSize);
// 計算擴容閾值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化數組
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
// 確保capacity爲大於或等於toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
// 返回最接近臨界值的2的N次方
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
計算元素在數組的具體位置
//簡單說就是取 hash 值的低 n 位。如在數組長度爲 32 的時候,其實取的就是 key 的 hash 值的低 5 位,作爲它在數組中的下標位置
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
// 簡單理解就是hash值和長度取模
return h & (length - 1);
}
添加到鏈表
ps:找到數組位置之後,就需要對key進行判重處理,如果有的話,就覆蓋重複key的值,返回舊值(判斷重複邏輯爲:可以的hash值相同,且原來key和當前key相等),如果沒有重複值,將新值放在鏈表的表頭
//主要邏輯爲,先判斷是否需要擴容,如果需要擴容就先擴容,最後再把數據封裝程Entry對象加入到鏈表表頭
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的數組位置已經有元素了,那麼要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容,容量 * 2
resize(2 * table.length);
// 擴容以後,重新計算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新計算擴容後的新的下標
bucketIndex = indexFor(hash, table.length);
}
// 創建元素
createEntry(hash, key, value, bucketIndex);
}
// 將新值放到鏈表的表頭,然後 size++
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++;
}
數組擴容
長度爲當前長度的2倍
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果之前的HashMap已經擴充到最大了,那麼就將臨界值threshold設置爲最大的int值
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對象
Entry<K, V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//當前對象的hash值
int i = indexFor(e.hash, newCapacity);
//頭插法,Entry對象放在新數組上第一位置,其他對象放在該對象的後一位置
e.next = newTable[i];
//將整體的對象放在指定的索引位置
newTable[i] = e;
//繼續循環下一個Entry
e = next;
}
}
}
get方法跟蹤
- 根據key計算出key的hash值
- 找到對應的數組下標
- 遍歷該數組下的鏈表,直到找到與之相等的key的值,或找不到返回null
//獲取數據
public V get(Object key) {
if (key == null)
//如果key爲null,就從table[0]獲取(put中,key爲null也是存儲在該位置)
return getForNullKey();
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
// 從鏈表中查詢數據
final Entry<K, V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 確定key對應的數組位置,遍歷鏈表直至找到,或者最終找不到返回null
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 != null && key.equals(k))))
return e;
}
return null;
}