一、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
的,但是对于计算机来说,&
运算的效率比取模运算的效率高。(这里也是一个注意点)