HashMap原理
先以一個簡單的例子來理解hashmap的原理。在Java中先隨機產生一個大小爲20的數組如下:
hash表的大小爲7,將上面數組的元素,按mod 7分類如下圖:
將這些點插入到hashmap中(簡單hashmap)後如下圖:
由上圖可知:
① hashmap是用鏈地址法進行處理,多個key 對應於表中的一個索引位置的時候進行鏈地址處理,hashmap其實就是一個數組+鏈表的形式。
② 當有多個key的值相同時,hashmap中只保存具有相同key的一個節點,也就是說相同key的節點會進行覆蓋。
③在hashmap中查找一個值,需要兩次定位,先找到元素在數組的位置的鏈表上,然後在鏈表上查找,在HashMap中的第一次定位是由hash值確定的,第二次定位由key和hash值確定。
④節點在找到所在的鏈後,插入鏈中是採用的是頭插法,也就是新節點都插在鏈表的頭部。
⑤在hashmap中上圖左邊綠色的數組中也存放元素,新節點都是放在左邊的table中的,這個在上圖中爲了形象的表現鏈表形式而沒有使用。
HashMap
上面只是簡單的模擬了hashmap 真實的hashmap的基本思想和上面是一樣的不過更加複雜。HashMap中的一個節點是一個Entity 類如下圖:
Entry是HashMap的內部類 包含四個值(next,key,value,hash),其中next是一個指向 Entry的指針,key相當於上面節點的值 value對應要保存的值,hash值由key產生,hashmap中要找到某個元素,需要根據hash值來求得對應數組中的位置,然後在由key來在鏈表中找Entry的位置。HashMap中的一切操作都是以Entry爲基礎進行的。HashMap的重點在於如何處理Entry。因此HashMap中的操作大部分都是調用Entry中的方法。可以說HashMap類本身只是提供了一個數組,和對Entry類中方法的一些封裝。
下面從源碼方面對 HashMap進行解析:
①HashMap的繼承關係
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
從上面可以看到HashMap繼承了AbstractMap , 並且實現了Cloneable, Serializable 接口。
②HashMap的構造函數
下面的代碼都是經過簡化處理的代碼,基本流程不變只是爲了更好的理解修改和刪除了一部分內容
public HashMap(int initialCapacity, float loadFactor) {
/*initialCapacity 初始化hashmap中table表的大小,前面的圖中左邊綠色部分的數組就是table。loadFactor填裝因子。
*/
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始化大小小於0,拋出異常
if (initialCapacity > 2^30)
initialCapacity = 2^30;
//HashMap 中table的最大值爲2^30。
/* 生成一個比initialCapacity小的最大的2的n次方的值,這個值就是table的大小。table就是一個Entry類型的數組。
*/
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
table = new Entry[capacity];
//新建一個Entry類型的數組,就是前面圖中左邊的數組。不過數組的元素是Entry類型的。
}
上面的代碼要做幾點說明:
①填裝因子:loadFactor 表示填裝因子的大小,簡單的介紹一下填裝因子:假設數組大小爲20,每個放到數組中的元素mod 17,所有元素取模後放的位置是(0–16) 此時填裝因子的大小爲 17/20 ,裝填因子就爲0.85啦,你裝填因子越小,說明你備用的內存空間越多,裝填因子的選定,可以影響衝突的產生,裝填因子越小,衝突越小。
②HashMap初始化過程就是新建一個大小爲capacity,類型爲Entry的數組,Entry上面已經介紹過這個類,包含一個指針一個key,一個value,和一個hash。capacity是2的次冪,至於爲什麼是2的次冪後面會有介紹的。
下面是另外兩個構造函數
public HashMap(int initialCapacity) {
HashMap(initialCapacity, 0.75);
//調用了上面的構造函數,只不過使用了默認的填裝因子0.75
}
public HashMap() {
HashMap(16, 0.75);
//生成一個table大小爲16,填裝因子0.75的HashMap
}
③由上可知如果用戶直接使用HashMap()構造函數來new一個HashMap 會生成一個大小爲16,填裝因子爲0.75的 HashMap。
③HashMap中的put(key,value)函數
還是先上源碼
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
/*如果key爲null則調用 putForNullKey(value) 函數 這個函數先在table[0]這條鏈上找有沒有key 爲null的元素如果有就覆蓋,如果沒有就新建一個new一個key爲null,value=value hash=0,的Entry放在table[0]。
*/
int hash = hash(key);
//獲得key的hash值
int i = indexFor(hash, table.length);
//由hash值確定放在table表中的那一條鏈上。類似於取模後放在數組中的哪個位置。
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;
//如果鏈上原來有一個hash值相同,且key相同的則用新的value值進行覆蓋。
}
}
//否則利用hash,key,value,new一個Entry對象插入到鏈表中。
modCount++;
addEntry(hash, key, value, i);
return null;
}
對上面代碼做幾點說明:
① HashMap中的key可以爲 null ,此時hash=0,爲什麼key可以爲null,因爲HashMap中放的元素是Entry,而Entry包含了4個值(key,value,hash,next),key爲 null 時不影響Entry映射到HashMap中。
②hash(key),產生一個正整數,這個整數與key相關。這個hash(key)函數比較關鍵,後面會進行說明。
③用戶插入的(key,value)對不是直接放到HashMap中的,而是用(key,value)以及後面由key value產生的hash,new一個Entry對象後再插入到HashMap中的。
④如果對應的鏈上有一個hash值個key相同的Entry則覆蓋value值,不new Entry對象,如果沒有會先new 一個對象在將其插到對應的鏈上。(其中可能會涉及到擴充HashMap)。
下面看看hash(key)函數
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
//hashCode 返回一個整數值,這個值跟對象有關,不同對象的hashCode值一般不同。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
hash函數的作用是使hashmap裏面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。
④HashMap中的get(Object key)函數
上源碼
public V get(Object key) {
if (key == null)
return getForNullKey();
//如果key==null則在table[0]這條鏈上找,如果找到返回value值,否則返回null ,因爲key==null的都是放在table[0]這條鏈上的。
Entry<K,V> entry = getEntry(key);
// getEntry(key)先key的hash值找到在數組的哪條鏈上,然後在鏈上查找key相同的如果沒找到返回null
//如果找到了返回Entry的value值。
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
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;
}
- 1
上面代碼的幾點說明
① 通過key來鏈表中查找元素包括兩個過程,先由hash找到鏈(hash由key產生,不同的key可能產生相同的hash值,相同的hash值放在同一條鏈上),再用key在鏈上找。
② 如果key爲null則只在table[0]和其鏈上查找,因爲key爲null都放在table[0]及其鏈上了。
③因爲在HashMap中查找到的是Entry對象,返回的值是Entry對象的value值。
重點Entry類
其實理解HashMap最重要的在於理解Entry類,Entry類相當於鏈表中的一個節點,是HashMap操作的基礎。下面主要從Entry類的幾個方法來理解Entry類和HashMap的關係。
①Entry中的addEntry( hash, key, value, bucketIndex)函數
在HashMap中調用put(key,value)時,如果(key,value)是首次加入到HashMap中,就會調用
addEntry( hash, key, value, bucketIndex)函數,將其加入到table表對應的位置中(注意是table中,不是後面的鏈中,首次加入的元素都是採用的頭插法)。下面是源碼:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
//如果size的值超過了threshold,將table擴容兩倍
hash = (null != key) ? hash(key) : 0;
//如果key爲null則hash=0,否則hash函數利用key來產生hash值。
bucketIndex = indexFor(hash, table.length);
//bucketIndex就相當於取模後對應的table表中的哪個位置。
}
//如果不存在容量不夠問題則直接新建一個Entry對象。
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//獲得原來首位的Entry對象
table[bucketIndex] = new Entry<>(hash, key, value, e);
//將新建的Entry對象放在鏈表的首位,然後用next指向原來放在首位的對象。也就是頭插。
size++;
}
上面代碼的幾點說明:
①bucketIndex是由hash取模後對應於table表中的哪個位置。indexFor(hash, table.length)其實是一個取模函數。它的實現很簡單 hash& (length-1),就是用hash值與上table表的長度減1。
②並不是對每一個(key,value)對都產生一個Entry對象,只是(key,value)對首次放到HashMap中時,或者HashMap中沒有相同的key時,才產生一個Entry對象,否則如果有相同的key則會直接將value值賦個Entry的value。
③新產生的Entry都是放在了table中,也就是鏈表的首位,採用鏈表的頭插法。
②HashMap中的keySet()函數
作用:返回HashMap中key的集合。keySet是HashMap中的內部類:
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
//如果keyset爲null就產生一個keyset對象。
}
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
//newKeyIterator迭代器用於遍歷key。
}
public int size() {
return size;
//返回keyset的大小
}
public boolean contains(Object o) {
return containsKey(o);
//是否包含某個key
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
//移除某個key的Entry。
}
public void clear() {
HashMap.this.clear();
}
}
keySet是用來遍歷整個HashMap的,因此是十分重要的,下面做幾點說明。
①keyset中有一個迭代器可以迭代的獲取下一個key的值,通過key的值就可以獲得Entry對象了。
②對應key的迭代遍歷是table表中由左向右,由上向下進行的,也就是先遍歷table[0]這條鏈上的,然後遍歷table[1]這條鏈上的依次往下進行。
③newKeyIterator具體實現這裏就不多介紹,只要知道上面的功能怎麼實現就可以了。
下面是利用keyset來實現遍歷HashMap的例子:
HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>();
for(int i=0;i<20;i++)
{
hashMap.put(i, i+1);
}
//新建一個hashmap往裏面放入20個(key,value)對。
Iterator<Integer> iterator= (Iterator<Integer>) hashMap.keySet().iterator();
//獲得keyset的iterator,進行遍歷整個hashmap。
while(iterator.hasNext())
{
Integer key=(Integer) iterator.next();
Integer val=(Integer)hashMap.get(key);
System.out.println(key+": "+val);
}
②HashMap中的entrySet()函數
作用:返回HashMap中Entry的集合。對於entrySet這裏就不上源碼了,舉一個使用entrySet遍歷HashMap的例子:
HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>();
for(int i=0;i<20;i++)
{
hashMap.put(i, i+1);
}
Iterator<Entry<Integer, Integer>> iterator=hashMap.entrySet().iterator();
while(iterator.hasNext())
{
Entry entry= iterator.next();
System.out.println(entry.getKey()+": "+entry.getValue());
}
使用entrySet()函數遍歷比keySet()函數遍歷快,因爲keySet()函數是先通過entrySet()求出key然後在通過key來遍歷獲得Entry的,所以速度比entrySet()慢很多。
hash(散列)衝突問題解決
- 拉鍊法:hashMap採用
- 線性探測法:實例ThreadLocal類中ThreadLocalMap採用的。