散列表又叫哈希表,是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱作散列函數,存放記錄的數組稱作散列表。java
在平常生活中,也有相似"散列表", 好比打籃球比賽,每一個球員穿着有號碼的球服,號碼錶明哪一個球員,球員若是有犯規,裁判用手勢指明幾號球犯規便可。說湖人24號球員,就是指科比,這裏24就是Key, 散列函數能夠理解爲f(x) = x, 通過24計算後獲得24,指向數組的24下標。算法
下面用一張描述一下散列表:數組
圖中hash方法是散列函數,散列函數在散列表中很是重要,決定了映射關係,決定了放在數組的位置。往散列表添加的過程是,好比添加key=24, value=kobe這個, key通過散列函數計算獲得index=2,就將它在數組下標爲2中。散列表中移除元素, 散列函數獲得2, 把下標爲2的元素設爲null。取值過程也同樣,24號是誰,hash(24)算一下獲得2,取出下標爲2的元素,返回value就可。散列表的操做是否是很簡單,其實不是的,剛剛只是理想的狀態, 隨着加入的元素愈來愈多就會產生一些問題。假如又加入一個元素, key爲99,hash(99)算出爲2,這是發現數組2的位置已經有元素,這種狀況就是哈希衝突,是因爲哈希函數,不一樣的key計算後獲得相同的值致使。有人就會說了設計一個不會衝突的函數就好啦,很差意思小聲告訴你,幾乎不存在這樣的函數。下面就講講什麼是哈希函數。數據結構
哈希函數有3點要求:函數
前1,2點相對容易實現,第3點前面爲何說幾乎不存在這樣的函數, 即便著名的MD5, SHA等哈希算法,也沒法徹底避免哈希衝突,只不過他們出現衝突的機率很是低而已。哈希衝突沒法避免是由於數組的長度有限,當你添加元素大於數組長度時,就確定出現會衝突。想象一下,你有3個籠子,有4個鳥,那麼確定有1個籠子有2個鳥。this
爲了解決哈希衝突,經常使用方法有兩類, 開放尋址法和鏈表法:spa
開放尋址法又能夠細分幾種,感興趣得本身查資料,這裏簡單說一下其中一種簡單的線性探測,插入數據時發現衝突,把index依次加1去找有沒有空的位置,有空位置就放進去,若是到數組最後都沒有,能夠數組index=0開始遍歷找。可是這種方法取數據時,若是遇到衝突時,相對效率會低一點,須要遍歷數組,找到key相匹配的元素;設計
鏈表法相對比較簡單,在相應的數組的index裏放一個鏈表,好比說, 放一個單鏈表, 衝突了就往單鏈表裏放便可。取數據時也是須要遍歷單鏈表, 找到key相匹配的元素。3d
當散列表中出現哈希衝突時,對於添加和獲取元素都會相應的下降效率,再也不是大O(1), 因此散列表要儘可能得較低衝突的發生, 哈希函數要儘可能保證均勻。 除了哈希衝突,數組會有滿的時候,滿了就須要擴容,可是擴容會使以前映射關係失效,須要從新進行hash(key)計算,從新計算出index。上面說了一堆理論,下面以java的HashMap爲例子,看看源碼是怎麼實現散列表的。指針
首先看構造函數(源碼版本是java 8)
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);
//loadFactor負載係數,
this.loadFactor = loadFactor;
//threshold臨界值
this.threshold = tableSizeFor(initialCapacity);
}
//tableSizeFor這個方法看起來可能會懵逼,
//這方法把一個數,先減1, 而後向右移1位再異或本身,
//好比0010右移1位是0001,異或後時0011,再異保證原來的位置和右移1位的位置都是1,
//接着繼續向右移2,4,8,16位並異或
//1+2+4+8+16=31,int類型一共是32, int最大值是2的32減1
//移16位並異或的n是保證了數的全部地位都是1
//MAXIMUM_CAPACITY = 2的30次方, 這是這方法的最大值
//若是小於MAXIMUM_CAPACITY,會進行n+1,n沒加1前全部的位都是1
//加1後,逢1進1,最後的獲得都是2的多少次方的值。
//若是cap = 9,tableSizeFor方法獲得是16。(16是2的4次方)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
HashMap構造器給兩個成員變量賦值,分別是loadFactor負載係數與threshold臨界值,而且threshold通過tableSizeFor方法獲得是一個int, 這個int不小於傳進去的數,並是一個2次方值。接着分析HashMap.put()
public V put(K key, V value) {
//調用hash方法,的到int類型的key, 再調用putVal
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
//key爲null時,返回0, 調用Object的hashCode方法,並異或(h >>> 16)
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//定義兩個數組tab和p變量
Node<K,V>[] tab; Node<K,V> p; int n, i;
//this.table是一個Node<K,V>[]成員變量
//tab = this.table, this.table數組賦值給tab
if ((tab = this.table) == null || (n = tab.length) == 0){
//第一次put數據時table爲null或者數組長度爲0進來這裏
//調用resize方法,會建立一個特定數組,並會賦值到this.table
//resize方法等會再細看, n等於this.table數組的長度
//tab = resize(), 保證tab變量不爲空
n = (tab = resize()).length;
}
//i = (n - 1) & hash 計算獲得i,i就是進過哈希函數後獲得數組的下標
// p = tab[index]
if ((p = tab[i = (n - 1) & hash]) == null){
// 數組的下標沒存放值,調用newNode建立一個Node<K,V>對象,存儲key,value
// 並賦值給這個數組的下標位置
tab[i] = newNode(hash, key, value, null);
}else {
Node<K,V> e; K k;
//p在前面if條件語句中賦值了,到這裏p不能爲空,p = tab[index]
//說明key進過哈希函數後獲得數組的下標,已經有值啦
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
//hash值相同, key是同一個指針或者用equals方法對比是否是相同的key
//這裏結合前面hash(Object key)和key.equals的判斷說明,使用hashmap時
//須要重寫obj的hashCode和equals方法來判斷是否是同一個key。
e = p;
}else if (p instanceof TreeNode)
//這裏是java8開始引入的TreeNode, TreeNode是用"紅黑樹"來代替鏈表
//這個先不深刻研究
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//當哈希衝突時,會走這個else代碼塊
for (int binCount = 0; ; ++binCount) {
//遍歷, p是當前數組下標的對象Node
//Node內部有next成員變量,next也是Node類型,
//Node是一個鏈表的結果
if ((e = p.next) == null) {
//newNode建立一個Node對象複製給next
//哈希衝突了,往鏈表裏添加
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
//TREEIFY_THRESHOLD = 8, 是一個常量臨界值
//treeifyBin(),當鏈表長度超過臨界值後會把鏈表轉成"紅黑樹"結構
//本文不深刻研究
treeifyBin(tab, hash);
}
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
if (e != null) {
//e 何時不爲null, key相同時
//key相同時,把value替換啦
V oldValue = e.value;
//onlyIfAbsent來控制是否舊的value替換新的value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//忽略,不研究
return oldValue;
}
}
++this.modCount;
//散列表的size加一
if (++this.size > this.threshold){
// size大於threshold,須要進行數組擴容和從新哈希映射
resize();
}
afterNodeInsertion(evict);
return null;
}
複製代碼
簡單看一下建立一個Node的代碼, Node是散列表元素, next是單鏈表。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
複製代碼
根據上面代碼總結一下添加元素的過程,Hashmap的Key和Value都是Object, Key的映射惟一性不能用對象自己,須要重寫hashcode方法保證與equal方法保證,Node裏成員變量hash 等於 (h = key.hashCode()) ^ (h >>> 16) 。 (n - 1) & hash 獲得就是數組的下標index。這裏設計有點巧妙的,h = key.hashCode()裏h是key的hash值,int有32位,h ^ (h >>>16)意思是低16位異或高16位,查了一下資料這樣處理好處是的獲得的數同時擁有低16位和高16位的特徵,保證了數值的隨機性,減低哈希衝突,這步計算稱爲擾動函數過程。 (n - 1) & hash 實質是求餘計算, 等價於 hash % (n -1) , 這裏n是數組的長度, 求餘後獲得的index就保證了必定在數組長度內。 (n - 1) & hash = hash % (n -1) 這個等價關係有個條件n必須是2次方, n - 1的值二進制裏的全部位置都爲1(好比16 - 1= 0000 1111)。回看到Hashmap的構造器裏tableSizeFor方法,就明白爲何了,就是要保證n的是2次方。若是調用Hashmap無參數構造器,默認是16。添加元素的過程當中,數組下標裏沒值,就放一個Node對象,若是有值就分爲2個兩種狀況,第一種key相同,替換value; 第二種若是key不相同,就是哈希衝突啦,追加Node鏈表裏,當鏈表中元素個數超過了一個臨界值,Node鏈表會換成紅黑樹結構TreeNode。
看看HashMap獲取元素過程
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k)))){
//對比key, 以及equals方法,key相同,返回Node的值
return first;
}
//first是Node的當前的值,first的hash和key都不是要拿的值
//從next鏈表指針中拿
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//若是是紅黑樹,從紅黑樹中找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍歷鏈表,從next指針從找key相同的,找到返回值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製代碼
前面說到當鏈表中元素個數超過了一個臨界值,Node鏈表會換成紅黑樹結構。紅黑樹也是基於鏈表實現一種數據結構,當哈希衝突比較多時,鏈表遍歷每次須要從鏈頭中獲取,效率低,換成紅黑樹是爲提升效率。紅黑樹本文不深刻,有興趣本身查查。下面看看resize方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = this.table;
//oldCap舊的數組長度,第一次爲0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//成員變量threshold賦值給oldThr
int oldThr = this.threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//newCap = oldCap << 1, 擴容oldCap乘以2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY){
//擴容, oldThr乘以2
newThr = oldThr << 1;
}
}
else if (oldThr > 0) {
//成員變量threshold賦值給oldThr
//oldThr賦值給newCap
newCap = oldThr;
}else {
//當調用了無參HashMap構造器是oldThr=0,
// newCap, newThr 設定默認值
newCap = DEFAULT_INITIAL_CAPACITY;
//newThr等於負載係數乘以默認數組長度
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//threshold等於負載係數乘以數組長度
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//建立新的數組Node
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍歷舊的數組
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//e = oldTab[j]
//舊的數組設爲null,清楚舊數組元素
oldTab[j] = null;
if (e.next == null){
//e.next == null意味着這個節點沒有哈希衝突,鏈表中沒數據
//e.hash & (newCap - 1)根據hash值從新計算新數組裏index
//把e賦值到新的數組裏
newTab[e.hash & (newCap - 1)] = e;
}else if (e instanceof TreeNode){
//有哈希衝突時,紅黑樹的處理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
}else {
// 有哈希衝突,鏈表中有數據, 遍歷鏈表,並遷移到新的數組中
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//若是爲0,說明擴容後索引的計算依然與擴容前一致
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//擴容後索引與擴容前不一致
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複製代碼
resize方法,第一次會建立一個新的數組,數組長度爲2次方值,每次擴容後數組容量右移1位,也就是乘以2,threhold = 數組容量 * 負載因子,在put方法的最後,若是size > threhold, 就須要擴容,HashMap負載因子默認爲0.75,意思是在添加數據的過程,當size 大於當前數組75%長度時,須要擴容到數組長度的2倍。擴容過程當中須要把舊數組的元素移到新數組中,舊數組的元素包含沒哈希衝突的元素和有哈希衝突的元素,沒哈希衝突的元素,根據新的數組長度從新配置index存放便可,有哈希衝突的還須要把鏈表或者紅黑樹的元素一併複製到新的數組中。 添加和獲取過程若是都理解啦,HashMap的移除過程也就很明朗啦,hash值獲得index,對比一下key是否相同,若是有衝突遍歷一下找到相同key, 找到元素移除便可,源碼就不列出來啦,HashMap的添加移除獲取過程大概原理就這樣啦。