一、HashMap內部的數據結構是什麼?
數組+單向鏈表
二、怎麼驗證內部結構是數組和單向鏈表?
a、數組:通過HashMap
源碼知道、HashMap
內部有個屬性 transient Node<K,V>[] table
b、單向鏈表:內部類Node
裏面維護了一個next
的屬性 Node<K,V> next
,是指向下一個節點的;
三、HashMap裏面爲什麼會有hash的存在?hash計算的理解?
我們先看下我的事例代碼
public class HashMapDemo {
public static void main(String[] args) {
HashMap<String,String> map=new HashMap<String, String>();
String keys="names";
String values="zhangsans";
map.put(keys,values);
System.out.println("數組的默認大小:"+ (1<<4));
System.out.println("對應二進制:"+binaryToDecimal((15)));
int hashCodes=keys.hashCode();
System.out.println("hashCode值:"+hashCodes);
System.out.println("對應二進制:"+binaryToDecimal(hashCodes));
int dw=(hashCodes >>> 16);
System.out.println("向右移16位,高位補0 值:"+dw);
System.out.println("對應二進制:"+binaryToDecimal(dw));
int yhValues=hashCodes ^ dw;
System.out.println("異或運算結果:"+yhValues);
System.out.println("對應二進制:"+binaryToDecimal(yhValues));
//(n:數組長度 - 1) & hash :hash值
System.out.println("下標計算:"+(yhValues & (16-1)));
}
/**
* 轉換二進制
* @param n
* @return
*/
public static String binaryToDecimal(int n){
String str = "";
for(int i = 31;i >= 0; i--){
int ys=(n >>> i & 1);
str = str + ys;
}
return str;
}
}
結果:
數組的默認大小:16
對應二進制:00000000000000000000000000001111
hashCode值:104585032
對應二進制:00000110001110111101011101001000
向右移16位,高位補0 值:1595
對應二進制:00000000000000000000011000111011
異或運算結果:104583539
對應二進制:00000110001110111101000101110011
下標計算:3
- 爲了Node節點數據落在何處 ;
- 當put數據的時候,我們通過源碼知道,會先經過數組,而數組的默認大小是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//這個時候我們知道了數組默認大小,那麼我們put的數據放在哪塊呢?隨機放?
a. 首先我們會 通過對 Object.hashCode() 得到一個整型數,【3373707】;
b. 根據數組默認大小,我們知道數組下標必須在0-15(擴容的下面說),那麼我們的算法肯定得控制在這個值之類,否則就會發生數組越界
c. 而hashCode是通過 key.hashCode()高16位和低16位進行異或運算得到一個整數類型的值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
d. 最後經過 & “與”運算,得到下標;
(n - 1) & hash
這個地方順帶說下:
這個也正好解釋了爲啥HashMap的數組長度要取2的整次冪。數組長度-1,正好相當於一個“低位掩碼”,然後呢, & “與”運算的結果就是散列值得高位全部歸零,只保留低位值;然後我們拿數組默認長度來說,16,下標就是 0-15;然後倒除法得到二進制值 1111
//高位全部歸零,只保留末四位
0000 0110 0011 1011 1101 0111 0100 1000
& 0000 0000 0000 0000 0000 0000 0000 1111
-------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
看到這裏,我們大概就會想到一個問題,這樣就算我的散列值分佈再鬆散,要是隻取最後幾位的話,碰撞也會很嚴重。分佈上成等差數列的漏洞,恰好使最後幾個低位呈現規律性重複。
這時候 “擾動函數" 的價值就體現出來了, 下面是所有值得二進制變化
h=hashCode() 0000 0110 0011 1011 1101 0111 0100 1000
—————————————————————————————————————————————————————————————————
h >>> 16 0000 0000 0000 0000 0000 0110 0011 1011
—————————————————————————————————————————————————————————————————
hash ^ hash >>>16 0000 0110 0011 1011 1101 0001 0111 0011
—————————————————————————————————————————————————————————————————
16-1 0000 0000 0000 0000 0000 0000 0000 1111
—————————————————————————————————————————————————————————————————
(16-1) & hash 0000 0000 0000 0000 0000 0000 0000 0011
—————————————————————————————————————————————————————————————————
3
在上面大家可以比對下 h
和 h >>> 16
是不是發現了一個很有意思的東西,自己的高半區和低半區做異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。
JDK8只做了一次干擾,爲什麼呢?推薦大家看下《An introduction to optimising a hashing strategy》的一個實驗應該就是明白了
e. 其實上面b-d可以直接替換成 Object.hashCode() % 16
這樣得到的結果和&
運算的結果都是保證在 0-15
的,但是對於計算機來說,&
運算的效率比取模運算的效率高。(這裏也是一個注意點)