HashMap详解
文章目录
HashMap<String, Object> objectHashMap = new HashMap<>();
1、实现原理
jdk1.7是基于哈希表(数组+链表)实现
jdk1.8是基于哈希表(数组+链表+红黑二叉树)实现
2、HashMap中数组的初始容量,加载因子是多少?
默认数组大小为16,默认加载因子0.75.
3、构造方法指定数组大小的结果
HashMap<String, Object> objectHashMap = new HashMap<>(10);
实际长度为16
HashMap<String, Object> objectHashMap = new HashMap<>(7);
实际长度为8
他是根据构造方法中的指定大小的值initialCapacity,计算大于initialCapacity最小的2的整数次幂的值为实际数组长度
4、HashMap的数组长度为什么需要是2的幂次方
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash % length,计算机中直接求余效率不如位移运算,源码中做了优化 hash & (
length - 1 ),
hash % length == hash & ( length - 1 ) 的前提是length是2的n次方; 为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;另外一个剪短的解释,2^n也就是说2的n次方的主要核心原因是hash函数的源码中右移了16位让低位保留高位信息,原本的低位信息不要,那么进行&操作另一个数低位必须全是1,否则没有意义,所以len必须是2 ^n ,这样能实现分布均匀,有效减少hash碰撞!
例如:长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
3 & ( 9 - 1 ) = 0 2 & ( 9 - 1 ) = 0
0000 0011 3 0000 0010 2
& 0000 1000 8 & 0000 1000 8
= 0000 0000 0 = 0000 0000 0
例如:长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
3 & ( 8 - 1 ) = 0 2 & ( 8 - 1 ) = 0
0000 0011 3 0000 0010 2
& 0000 0111 7 & 0000 0111 7
= 0000 0011 3 = 0000 0010 2
测试两种运算的效率差
/**
* 直接【求余】和【按位】运算的差别验证
*/
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
int num1=0;
int count = 10000*10000;
for (long i = 0; i < count; i++) {
num1=9999%1024;
}
long time2 = System.currentTimeMillis();
int num2=0;
for (long i = 0; i < count; i++) {
num2=9999&(1024-1);
}
long time3 = System.currentTimeMillis();
System.out.println("最后的结果 num1="+num1+",num2="+num2);
System.out.println("% 运算的时间为: "+(time2-time1));
System.out.println("& 运算的时间为: "+(time3-time2));
}
>>> 最后的结果 num1=783,num2=783
>>> % 运算的时间为: 359
>>> & 运算的时间为: 93
5、HashMap怎么实现的长度是2的幂次方
/**
* 方法保证了HashMap的哈希表长度总位2的幂次方
* 返回大于输入参数且最近的2的整数次幂的数
*/
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;
}
本质思想是想把cap-1后的二进制值,最高位1的后面全部利用或运算变成1,最后结果再加1,这样就可以得到最小的大于cap的2的幂次方。
例如:
假如cap输入18,那么cap的二进制10010,n=cap-1的二进制10001
第一次 右移1个 10001 | 01000 = 11001
第二次 右移2个 11001 | 00110 = 11111
第三次 右移4个 11111 | 00001 = 11111
第四次 右移8个 11111 | 00000 = 11111
第五次 右移16个 11111 | 00000 = 11111
最后判断如果n在0-1<<30(相当于2的31次幂)的区间内,那么n+1也就是 100000
输出32
程序测试
public static void main(String[] args) {
int num = 18;
int result = tableSizeFor(num);
System.out.println("最后输出 "+result);
}
static final int tableSizeFor(int cap) {
System.out.println("cap的二进制是"+Integer.toBinaryString(cap));
int n = cap - 1;
System.out.println("n=cap-1的的二进制是"+Integer.toBinaryString(n));
System.out.print("第一次 右移1个 "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>1));
n |= n >>> 1;
System.out.println(" = "+Integer.toBinaryString(n));
System.out.print("第二次 右移2个 "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>2));
n |= n >>> 2;
System.out.println(" = "+Integer.toBinaryString(n));
System.out.print("第三次 右移4个 "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>4));
n |= n >>> 4;
System.out.println(" = "+Integer.toBinaryString(n));
System.out.print("第四次 右移8个 "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>8));
n |= n >>> 8;
System.out.println(" = "+Integer.toBinaryString(n));
System.out.print("第五次 右移16个 "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>16));
n |= n >>> 16;
System.out.println(" = "+Integer.toBinaryString(n));
System.out.println("最后n+1的二进制是 "+Integer.toBinaryString(n+1));
//判断如果n在0-1<<30(相当于2的31次幂)的区间内,将n+1
return (n < 0) ? 1 : (n >= 1<<30) ? 1<<30 : n + 1;
}
6、HashMap的put方法
/**
* Implements Map.put and related methods.
*
* @param hash hash for key #传入参数key的特殊hash值
* @param key the key #传入的参数key
* @param value the value to put #传入的参数value
* @param onlyIfAbsent if true, don't change existing value #默认为false,如果为true,不改变原有value值
* @param evict if false, the table is in creation mode.#如果为false则表现创建模式
* @return previous value, or null if none #返回以前的值,如果没有,则返回null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
先说HashMap的Put⽅法的⼤体流程:
-
根据Key通过哈希算法与与运算得出数组下标
-
如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置
-
如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对
象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过
程中会判断红⿊树中是否存在当前key,如果存在则更新value
ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插
⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊
到链表后,会看当前链表的节点个数,如果⼤于等于8,且数组长度大于等于64,那么则会将该链表转成红⿊树
iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就
扩容,如果不需要就结束PUT⽅法
7、计算key位置的hash函数如何设计的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
8、为什么这么设计hash函数
hashcode为int类型,4个字节32位,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。
由于绝大多数情况下数组length一般都小于2^16即小于65536。 当数组length=2的N次方, 下标运算结果取决于哈希值的低N位。 所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。
为了让高16位也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位 ^ 运算。所以(h >>> 16)得到他的高16位与hashCode()进行 ^运算。
例一个key的hash值:
h:0110 0100 1000 1111
0110 1001 0010 0101
h>>>16:0000 0000 0000 0000 0110 0100 1000 1111
这样进行^运算,保证了 32位全部进行计算。
(1)由于大多数取结果的后16位,这样保证了hashcode32位全部参与计算,也保证了0,1平均,散列性
(2)结果的前16位保证hashcode前16位了0,1平均散列性,附带hashcode前16位参与计算。
(3)16与16位数相同,利于计算,不需要补齐,移去位数数据 更多情况,hashmap只会用到后16位(临时数据一般不会这么大)
(4)利用了扰动函数
1、降低hash碰撞,尽可能分散
2、使用位运算,使得算法高效
9、为什么用^而不用&和|
因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。
感谢:
部分图片引用自:https://blog.youkuaiyun.com/loveyouyuan/article/details/108104754
第四题参考:https://blog.youkuaiyun.com/sidihuo/article/details/78489820