**
一、数据结构不同:
**
1.7:数组+链表 头插法(向链表中添加元素时在头部插入,这样能提高插put操作的性能,但在多线程情况下有链表逆序和循环链表的问题–会导致死循环)。
Entry<K,V>数组
static class Entry<K,V> implements Map.Entry<K,V>{
final K key;
V value;
Entry<K,V> next;
int hash;
}
1.8:数组+链表+红黑树 尾插法(插入时是在尾部插入)。
Node<K,V>数组 不过这个Node可以为链表结构,也可以为红黑树结构。
链表结构:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
红黑树结构:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
LinkedHashMap.Entry<K,V>实际上是继承了上面Node<K,V>的结构,不过多了两个before和after节点。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
二、hash值的计算逻辑不同:
1.7:基于key自身的hashCode方法的返回值又进行了一些位运算,目的是为了随机和均匀性。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
假设此时table[]的length是16,那么对于key1来说就是
0AC20000 & 0000000F = 0
对于key2来说
017F0000 & 0000000F = 0
也就是说key1和key2的存储位都是0,发生了hash冲突。
11280384和1568768这两个hashcode明显是完全不同的,但却发生了hash冲突,是因为这两个hashcode的低位相同,高位不同。而HashMap在计算index直接取模就是只取了低位,如果不对hashcode进行高低位扰动计算,就会极大增加了发生hash冲突的几率。
我们按上面的代码一点点分析,加入扰动函数之后:
0AC20000 >>> 20 = 000000AC
0AC20000 >>> 12 = 0000AC20
000000AC ^ 0000AC20 = 0000AC8C
0AC20000 ^ 0000AC8C = 0AC2AC8C
看到这里应该明白了,这段扰动函数的目的是把hashcode的高低位混在一起,让高位发生变化时,低位同时也发生变化。
接下来又进行了一些更多的扰动计算:
0AC2AC8C >>> 7 = 00158559
0AC2AC8C >>> 4 = 00AC2AC8
0AC2AC8C ^ 00158559 ^ 00AC2AC8 = 0A7B031D
所以对于key1来说,扰动完成后的hash是0A7B031D
而key2进行相同的扰动计算后,hash是016A18B6
这样key1的存储位是13,key2是6,没有发生hash冲突。
1.8:直接拿key的hash值做高低位异或操作。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
寻址计算时,能够参与到计算的有效二进制位仅仅是右侧和数组长度值对应的那几位,意味着发生碰撞的几率会高。
通过移位和异或运算让hashCode的高位能够参与到寻址计算中。
采用这种方式是为了在性能、实用性、分布质量上取得一个平衡。
有很多hashCode算法都已经是分布合理的了,并且大量碰撞时,还可以通过树结构来解决查询性能问题。
所以用了性能比较高的位运算来让高位参与到寻址运算中,位运算对于系统损耗相对低廉。
三、table 的初始化
1.7 初始值指向一个空数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
1.8会在resize()时初始化。
transient Node<K,V>[] table;
四、增删改查的方式不同
由于数据结构不同,所以这四种操作其实都做了优化。
值得一提的是在JDK8中,当链表长度达到8(TREEIFY_THRESHOLD )但数组的容量少于64(MIN_TREEIFY_CAPACITY )时会先进行扩容操作。大于等于64时才会转为红黑树。
小问题:为什么hashmap中数组的长度必须为2的整数次幂
答:hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1)。
hash%length==hash&(length-1)的前提是length是2的n次方;
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
其实就是按位“与”的时候,每一位都能 &1
X % 2^n
= X & (2^n - 1)。
2^n
表示2的n次方,也就是说,一个数对2^n
取模 == 一个数和(2^n - 1)做按位与运算 。
假设n为3,则
2^3
= 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。
此时X & (2^3 - 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
先写到这儿,如果JDK1.7与1.8关于HashMap的实现有其他大的区别,希望大家能帮忙补充。
参考文章
1.8源码解析
JDK1.8中HashMap如何应对hash冲突?
详细梳理JAVA7和JAVA8 HashMap的hash实现