第一章 JAVA集合之HashMap原始碼淺析
屌絲程式設計師的奮鬥之路現在開始
java集合這一塊無論在面試或在寫程式碼中,我們都會接觸到,所以java集合是特別重要的,其中HashMap更是被我們經常用到。
一.概括
HashMap是用鍵值對的既已key-value的形式來儲存值的,當然這只是展現給大家的一種表象,key和value都可以為空,但是key不能重複,HashMap不是現線安全的,如果想讓HashMap變成現線安全的,可以呼叫Collections的靜態方法synchronized方法。其實HashMap是用一個動態陣列和多個連結串列來存放key-value的
二.HashMap的資料結構
HashMap可以說是由一個動態陣列和多個連結串列組成,連結串列是接在每一個數組單元下面的,動態陣列和連結串列中儲存的單元是一個叫Entry的物件,從下面的圖中可以很直觀的看出HashMap的資料結構,其中每一個單元格儲存的就是Entry物件了,這一個Entry物件是HashMap的一個靜態類
Entry原始碼
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//指向一下個Entry物件,他是為解決hash衝突而存在的。 int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
從Entry的屬性中看到了我們所熟悉的key和value,沒錯,這就是我們在用HashMap的時候所要接觸到的key,value,Entry對key-value進行了封裝,我們再看看Enrty的next屬性,儲存的就是指向下一個物件的指標,當然java是沒有指標這一說的,我覺得在這裡將它當成指標更好理解,next在出現hash衝突的時候會發生作用,現在我們再看看上面的那一張圖,現在知道為什麼那些綠色的連結串列是怎麼連線起來的了吧,就是通過Entry的next屬性指向下一個Entry物件連線起來的,所以在HashMap原始碼中是看不到動態連結串列的定義,但是它確實是存在的。
三.HashMap的API
1.HashMap的相關屬性
/**
* HashMap中陣列的預設大小是16
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 陣列的最大長度
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 預設的載入因子是0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 存放Entry物件的陣列,也是HashMap存放資料的地方
*/
transient Entry<K,V>[] table;
/**
* HashMap的存入值得個數,注意:他和陣列的大小是沒有關係的
*/
transient int size;
/**
* 邊界值 <span style="font-family: Arial, Helvetica, sans-serif;">邊界值=HahsMap的容量*載入因子</span>
* @serial
*/
int threshold;
/**
*載入因子
* @serial
*/
final float loadFactor;
邊界值=陣列大小*載入因子
當HashMap所儲存物件的個數超過邊界值的時候就會對陣列進行擴容,例如HashMap預設的載入因子是0.75,陣列預設的大小是16,所以邊界值是12,當我們在HashMap中儲存的值大於等於12的時候,HashMap會對陣列table進行2倍的擴容。
2.HashMap的構造方法
/**
*給陣列設定初始容量和載入因子
*/
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;
/*
*將陣列的容量設定為大於初始容量的最小2次冪
*例如你給HashMap設定的初始容量是20,那HashMap會自動將容量變為32
*/
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
/**
*如果只設置HashMap初始大小,就用預設的載入因子:0.75
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
*給HashMap設定成預設的大小:16,預設的載入因子0.75
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
*將Map集合存入HashMap
*/
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(int initialCapacity, float loadFactor)進行初始化的時候,HashMap裡面陣列的大小不是我們設定的initialCapacity值,而是大於initialCapacity的最小2次冪。
3.HahMap的hash算
看的不是太懂,需要知道HahMap就是根據key值來進行hash計算的
/**
*HashMap的hash演算法
*/
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// 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);
}
4.HahMap的取值方法:get(Object key)
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
我們先看看getEntry這個方法
final Entry<K,V> getEntry(Object key) {
//對key進行hash計算得到hash值
int hash = (key == null) ? 0 : hash(key);
//再用hash值對資料長隊進行取模運算得到key在陣列的儲存位置,再遍歷以陣列這個位置為頭結點的連結串列
for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
Object k;
//先去比較key的hash值是否相等,相等再去比較key值是否相等,如果兩個都相等,才算找到了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
這裡在比較key值是否相等的時候,前面為什麼還要比較hash值是否相等,我覺得是用hash值比較更加快速,能快速的排除不相等的物件。
再看看getForNullKey這個特殊的方法
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
可以看到是直接就定位到了table[0]這個地方,說明當我們在儲存key=null的鍵值對的時候,HashMap是直接放在table[0]這個連結串列中的
5.HahMap的存值方法:V put(K key, V value)
先用圖來說明put方法的大體過程,再看原始碼put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。如下:
首先我們假設一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:
然後我們插入一個數:put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數,程式對該“連結串列”進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。
在put(33,33),key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
/*
*用陣列長度對key的hash值進行取模運算,得到key對應陣列的某一個位置
*再對以這個陣列元素為頭結點的連結串列進行遍歷
*/
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果HahMap中有key的存在,就將新的value替換舊的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//這個方法沒有做任何操作
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//將新新增的key—value放在table[i]的位置
addEntry(hash, key, value, i);
return null;
}
我們先看indexFor方法,indexFor方法是如何利用陣列長度對hash值進行取模的
static int indexFor(int h, int length) {
return h & (length-1);
}
很簡單,對不對,但這裡面卻蘊含著大智慧,首先&運算是要比%這種運算要快很多的,還有這個length這個值始終是2的n次冪,我們前面講到了當在運用HashMap的構造方法的時候給table設定初始值,table的長度是大於這個初始值的最小n次冪,length-1一定是111...11這樣的二進位制,這樣就再對hash值取模的時候資料的每一個地方都是可以達到的。這樣就會在儲存值得時候減少hash衝突。
addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
//先比較size和邊界值的大小
if ((size >= threshold) && (null != table[bucketIndex])) {
//如果當size大於等於邊界值的時候,會對陣列進行2倍擴容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//從新計算key-value存放到陣列的地方
bucketIndex = indexFor(hash, table.length);
}
//將新加入的key-value放入到陣列中
createEntry(hash, key, value, bucketIndex);
}
createEntry方法//將新加入的key-value放到table的陣列中,再將新加入的Entry的next指向陣列原來的位置的值,這樣就形成了連結串列
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer方法,從新計算原來陣列的元素在新陣列元素中的位置
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍歷table陣列
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);
}
//從新計算e在新陣列的位置
int i = indexFor(e.hash, newCapacity);
//e的next指向原先newTable[i]
e.next = newTable[i];
//將e放入陣列先的位置
newTable[i] = e;
e = next;
}
}
}
總結一下HashMap的存值的過程
1.首先定位key對應陣列中的某一個位置
2.在遍歷一下以這個位置的元素為表頭的連結串列
3.檢視這個連結串列中是否有同樣的key值
3.1 如果有,就用的新的value替換舊的value,到此就結束了
3.2如果沒有,就將新的key-value放入到陣列中
4.如果要放到陣列中,首先會判斷HashMap儲存的值得個數是否大於等於邊界值
4.1 如果大於邊界值,會對陣列進行2倍擴容,擴容後會重新計算以前HashMap在新的陣列中的位置
5.將新加入的Entry放入到根據對陣列相應的位置上,再讓Entry的next屬性指向原來的陣列元素
四.總結
HashMap的資料結構就是由一個數組和多個連結串列組成的,陣列和連結串列中儲存的元素是Entry物件,Entry中有key,value,next,hashCode這幾個屬性,我們向HashMap中存放key-valu的其實是存入到了Entry物件中了。
HashMap是對key的hashcode進行hash計算得到一個hash值,再用這個hash值與陣列長度減一進行於運算,得出key存在陣列中的某一個位置,如果陣列的這個位置已經有值了,這就產生了所謂的hash衝突,HashMap會將新加入的Entry放在陣列中,並讓Entry的next指向以前的陣列元素,這樣就在這裡產生了連結串列。
在新加入元素的時候,當HashMap儲存值的個數即size大於或等於邊界值的時候,就會對陣列進行2倍擴容,這裡就是HashMap比較消耗新能的地方了,因為擴容後不僅要遍歷整個HashMap,而且還要重新計算每個元素在新的陣列中的位置。所以我們在初始化HashMap的時候可以指定陣列的大小,儘量減少陣列擴容。