HashMap原始碼解讀
介紹了ArrayList和LinkedList,就兩者而言,反應的是兩種思想
1> ArrayList底層是以陣列構成的,查詢和在不擴容的情況下,順序新增元素很快,插入和刪除較慢
2> LinkedList底層是雙向連結串列實現的,查詢需要從header節點向前或向後遍歷,插入和刪除元素快
是否存在一個集合具備上面兩個的優點,就是HashMap
HashMap是一種key-value形式儲存的資料結構
HashMap的關注點
是否允許為空 | key和value都允許為空 |
是否允許重複 | key重複會覆蓋,value允許重複 |
是否有序 | 無序,遍歷得到的順序基本上不可能是put的順序 |
是否執行緒安全 | 非執行緒安全 |
HashMap的資料結構
java中,最基本的結構就是兩種,一個是陣列,另一個是模擬指標(引用),HashMap實際上一個連結串列雜湊的資料結構,就是陣列和連結串列的結合體
從上圖可以看出,HashMap底層就是一個陣列結構,陣列中的每一項又是一個連結串列,新建一個HashMap的時候,就會初始化一個陣列.
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
從原始碼可以看出,hashmap的底層是一個Entry陣列,每一個entry儲存了key-vlaue 和下一個entry的引用,是個連結串列結構
HashMap的儲存
public V put(K key, V value) {
//如果key是null,將這個entry放在table[0]的位置
if (key == null)
return putForNullKey(value);
//根據key的hashcode計算hash值
int hash = hash(key.hashCode());
//根據key的hash值計算出在陣列中的索引位置
int i = indexFor(hash, table.length);
//遍歷entry[]索引的連結串列,找到hash值和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;
}
}
//否則,就記錄下修改次數,在這個索引位置加上一個節點
modCount++;
addEntry(hash, key, value, i);
return null;
}
整個過程:
根據key計算他的hash值,找到在陣列中的位置(即下標),如果該陣列改位置上已經有元素,就會已連結串列的形式儲存,新加入的放在前面,原來的放後面,如果改位置上沒有元素,就會建立一個元素放在改位置。
下面詳細的介紹下,每個方法是怎麼執行的
第一步: 如果key是null,從table[0]這個索引,找到連結串列進行遍歷,如果找到節點的key為null,就將value替換原來的value,
如果table[0]處的節點為空,就建立節點放到哪裡,後面檢查size+1是否超過容量*載入因子的值,超過的話按2倍大小擴容。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//每次呼叫put方法重寫節點的value,都會呼叫
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
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);
}
第二步:計算hash值,找到索引位置,遍歷索引處的連結串列,找到相同的key,進行替換。
static int hash(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);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
在hashmap中要找到某個元素,需要根據key的hash值求得在陣列中索引位置,使用hash演算法求的這個位置的時候,檢查這個位置上的連結串列是否存在相同的key,存在的話,替換value,否則,就是在這個索引位置新增元素。
在陣列索引位置處新增節點
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);
}
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++;
}
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
對於createEntry方法
如果在某個索引位置處沒有元素,執行put("111","111")就建立一個節點e1,此時next = null,table[2]=e1
假設如果執行put節點e2,也放在這個索引位置,此時next=table[2],就是e1,然後有table[2] = e2
結論:在一個索引位置,每新增一個元素,就會將table[index]指定它,新增的這個元素的next執行原來table[index]的元素
,形成了單向連結串列
為什麼HashMap的長度是2的n次方
hashmap預設長度是16,其他情況取的是大於設定容量的2的n次方的最小值
當長度是2的n次方時候,能夠減少元素得到的陣列下標在同一個位置的概率,減少碰撞。
h & (table.length-1) hash table.length-1
4 & (15-1) 0100 1110 = 0100
5 & (15-1) 0101 1110 = 0100
4 & (16-1) 0100 1111 = 0100
5 & (16-1) 0101 1111 = 0101
和15-1運算的後果是0001,0011,0111,0101,1001,1101這幾個位置都無法存放元素,因為運算的結果根本就不會算到這個索引上,不僅浪費了空間,還增加了元素碰撞的效率, 降低了查詢效率
長度是2的n次方長度,(length-1)每個位都是1,因為hash值是均勻分佈的,不同key算到的結果相同的概率很小,碰撞的機會少,效率更高
讀取
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;
}
有了前面的介紹,後面看起來就比較簡單了,如果是null,從table[0]找到key為null的節點,否則返回null
key不為null,計算hash值找到在陣列中的下表,從這個位置連結串列找到hash和key相同的節點,返回value,否則返回null
歸納起來說,HashMap在底層將key-value當做一個整體進行處理,這個整體是一個Entry物件。HashMap底層採用一個Entry[]陣列儲存所有的key-value對,當需要儲存Entry物件時,根據hash演算法決定其在陣列中的儲存位置,在根據equals方法找到連結串列上的儲存位置;當需要取出一個entry時候,也是根據hash演算法找到陣列上的位置,equals方法找到節點,取出entry
HashMap的擴容
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);
}
threshold = int(capacity*loadFactor)
當陣列中元素的個數大於等於threshold(容量*loadfactor),就會按照原來容量的2倍進行擴容
hashMap的效能引數
hashMap包括下面幾個構造器
new HashMap() 初始容量16,loadfactor(載入因子)為0.75
new HashMap(int capacity) 初始容量為大於capacity的2的n次方的最小值,loadfactor為0.75
new HashMap(int capacity,float loadFactor)
loadFactor:陣列中entry元素的個數除以總容量,載入因子如果過大,對空間利用率高,但是查詢效率低,反之,載入因子小,浪費空間.
Fast-fail 機制
transient volatile int modCount;
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
在多執行緒環境下如果使用迭代器迭代,存在另一個執行緒修改了資料結構(put,remove),就會導致當前執行緒迭代器出現modCount!=exceptedModCount,並丟擲異常。迭代器就會快速失敗。
HashMap和HashTable的區別
HashTable和HashMap是一組相似的鍵值對集合,主要的區別
1. HashTable是執行緒安全的,通過synchronized鎖保證執行緒安全,HashMap則是執行緒不安全的
2. HashTable不允許key為null,HashMap允許key為null
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
3. HashTable和HashMap的hash演算法不同
參考部落格:http://www.cnblogs.com/xrq730/p/5030920.html
相關文章
- HashMap原始碼個人解讀HashMap原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- HashMap 原始碼閱讀HashMap原始碼
- HashMap原始碼閱讀HashMap原始碼
- Java-HashMap中put原始碼解讀JavaHashMap原始碼
- HashMap原始碼解析和設計解讀HashMap原始碼
- HashMap原始碼詳解HashMap原始碼
- java 8 HashMap 原始碼閱讀JavaHashMap原始碼
- 閱讀原始碼,HashMap回顧原始碼HashMap
- 詳解HashMap原始碼解析(下)HashMap原始碼
- 詳解HashMap原始碼解析(上)HashMap原始碼
- HashMap原始碼分析(二):看完徹底瞭解HashMapHashMap原始碼
- JDK原始碼閱讀(4):HashMap類閱讀筆記JDK原始碼HashMap筆記
- 原始碼分析——HashMap原始碼HashMap
- HashMap 原始碼分析HashMap原始碼
- HashMap原始碼剖析HashMap原始碼
- HashMap原始碼整理HashMap原始碼
- HashMap原始碼分析HashMap原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- Java——HashMap原始碼解析JavaHashMap原始碼
- 原始碼分析之 HashMap原始碼HashMap
- Java:HashMap原始碼分析JavaHashMap原始碼
- 搞懂 Java HashMap 原始碼JavaHashMap原始碼
- 學習HashMap原始碼HashMap原始碼
- Laravel 原始碼解讀Laravel原始碼
- reselect原始碼解讀原始碼
- Swoft 原始碼解讀原始碼
- Seajs原始碼解讀JS原始碼
- ReentrantLock原始碼解讀ReentrantLock原始碼
- MJExtension原始碼解讀原始碼
- Axios 原始碼解讀iOS原始碼
- SDWebImage原始碼解讀Web原始碼
- MJRefresh原始碼解讀原始碼
- Handler原始碼解讀原始碼
- LifeCycle原始碼解讀原始碼
- LinkedHashMap原始碼解讀HashMap原始碼
- ConcurrentHashMap原始碼解讀HashMap原始碼
- Redux原始碼解讀Redux原始碼