Java hashmap原理
線性表:存儲在連續的內存地址,查詢快,插入和刪除慢。
鏈式表:存儲在間斷的,大小不固定,插入和刪除快,但是查詢的速度慢。
hashmap是以上兩種者折中的解決方案,插入或者刪除只需要動一部分即可。
HashMap的基本原理:(jdk1.7)
HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。
當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。
- 先判斷key 是否爲null
- 再根據key 調用hashCode()進行定位
在jdk1.8引入了紅黑樹
hashmap的主要參數
//默認初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//容量最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子0.75 (當前已佔當前最大容量的75%,會進行擴容操作)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//樹化的閾值,當桶中鏈表節點數大於8時,將鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//紅黑樹退化爲鏈表的閾值,當桶中紅黑樹節點數小於6時,將紅黑樹轉換爲鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//最小的樹化容量,進行樹化的時候,還有一次判斷,只有鍵值對數量大於64時纔會發生轉換,
//這是爲了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表而導致不必要的轉化
static final int MIN_TREEIFY_CAPACITY = 64;
單個結點的屬性有:
hash:用於快速定位;
key:標識符
Value:存儲的數值
next:引用地址,便於插入、刪除操作
補充:
爲什麼hashmap的長度爲2的n次方?
通過hash進行定位時,hash%length 速度偏慢
若是2的n次方 hash%length==hash&(length-1) 而&操作顯然更快。
get操作:get(k)
1.判斷表是否爲空或者待查找的桶不爲空 (會先使用hashCode()計算出hash值進行哈希桶)
2.首先檢查待查找的桶的第一個元素是否是要找的元素,如果是直接返回
3.桶內紅黑樹,則調用getTreeNode()查找紅黑樹
4.桶內是鏈表,遍歷鏈表尋找節點
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//表不爲空&&表長大於0&&待查找的桶不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//首先檢查桶中的第一個節點,如果相等,則直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果桶中是樹結構
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//桶中是鏈表,則遍歷鏈表如果找到則直接返回
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
put操作:put(k,v)
1.如果表爲空或者表的長度爲0,調用resize初始化表,爲表分配空間
2.①二次散列處的桶爲空,直接插入元素
②桶不爲空
a)桶處的第一個節點與待插入節點的哈希相同且key“相等”,直接賦給變量e
b)桶中是紅黑樹,調用putTreeVal插入紅黑樹中
c)桶中是鏈表,遍歷鏈表,如果其中存在相同的key,則賦給變量e;不存在則尾插法加入鏈表,並判斷節點數是否大於8,如果大於8則調用treeifyBin()轉化爲紅黑樹
3.①e不爲空,替換其中的value值,並返回舊的value值
②e爲空,表大小+1,判斷是否達到了閾值,如果達到了則需要擴容
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果表爲空或者表的容量爲0,resize初始化表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根據hash得到在表中索引位置的桶,如果桶爲空,則將節點直接插入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//桶不爲空
else {
Node<K,V> e; K k;
//首先判斷桶中第一個節點的hash與待插入元素的key的hash值是否相同且key是否"相等",如果相等,賦給變量e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//是樹節點,則調用putTreeVal添加到紅黑樹中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否則是鏈表,遍歷鏈表,如果不存在相同的key,則插入鏈表尾部,並且判斷節點數量是否大於樹化閾值,如果大於則轉換爲紅黑樹;如果存在相同的key,break,遍歷鏈表結束
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不爲空表示存在相同的key,替換value並返回舊值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//鏈表元素增加,並判斷是否大於閾值,如果大於,則擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
補充說明:
在插入新節點時,JDK7採用的是頭插法,JDK8採用的是尾插法。
採用頭插法的原因是爲了熱點數據(新插入的數據)能夠優先被查找,但是這種做法有可能在多線程的情況下可能會導致死鎖( 產生死循環)。
鏈表查詢的時間複雜度是n,而紅黑樹查詢的時間複雜度是log^n
其他方法:
鏈表轉化爲紅黑樹:treeifyBin()
擴容操作:resize()
hashmap解決哈希衝突
開放定址法( 線性探測再散列,二次探測再散列,僞隨機探測再散列)
發生衝突時,形成探索序列,沿此序列逐個單元地查找,直到找到給定的哈希地址,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則表明表中無待查的哈希地址,即查找失敗。
鏈地址法: Java hashmap就是這麼做的
將所有哈希地址相同的結點鏈接在同一個單鏈表中。
線程安全
避免多線程對共享數據進行操作時,產生的髒讀、誤讀。
Hash線程安全:
1.多線程對相同key進行put操作
2.多線程同時對Node數組進行擴容,導致數據丟失
三種線程安全的方法:
a.hashtable 通過關鍵字synchroized標記臨界區來實現
b.ConcurrentHashMap jdk1.8引入CAS算法 它的性能最好 (CAS鎖是一種樂觀鎖,即輕量級別的鎖)
c.SyschroniedMap 通過類間接使用 syschroized
補充:syschronied的方式是一種悲觀鎖(是重量級鎖)
hashmap的常用方法:
存值: map.put(key,value)
讀值: map.get(key)
判斷是否爲空:map.isEmpty()
判斷是否含有key:map.containsKey(key)
判斷是否含有value:map.containsValue(value)
刪除key值下的value:map.remove(key) //只刪除了Value
顯示所有value值:map.values()
元素的個數:map.size()
顯示所有key:map.keySet()
顯示所有key和value:map.entrySet()
合併2個類型相同的map:map.putAll(map1)
刪除這個key和Value:map.remove(key,value)
替換key下的value:map.replace(key,newValue)
清洗整個map:map.clear()
map的克隆:map.clone()
使用hashmap時需要同時重寫 HashCode和equals方法。
單一重寫HashCode和equals都不能保證數據的唯一性。
Hashcode()的作用:將對象的內部地址轉換成一個整數返回。(即獲取hash桶的編號)
equals()的作用:只是比較2個對象之間的內容是否相同
重寫時注意:
1. equals true ->hashcode返回的int要相同
2. equals false ->hashcode 返回的int一定不相同
3. hashcode int 相同 ->equals 可以是true 也可以是false
4. hashcode int 不同 ->equals一定要是false
這裏結合 equals的知識,equals先比較 引用類型是否一致,如果一直則再比較其值是否相等。
由於HashMap不支持基本數據類型(如int),但是支持封裝類(如 Integer)
HashMap 同一個Hash桶(hashcode)裏的數據類型必須是相同的
比較 HashSet 和 HashMap
HashSet:
1.必須要重寫hasdcode 和 equals 從而確保對象的唯一性
2.利用對象獲取hashcode 元素以對象的形式
3.add()增加元素
HashMap:
1.必須要重寫hashcode 和equals
2.利用key獲取hashcode 以<key,value>的形式
3.put()增加元素