HashMap在JDK1.7和JDK1.8的区别
一.区别
- 存储结构不同:JDK1.7是数组+链表,JDK1.8则是数组+链表+红黑树结构;
- 初始化方式不同:JDK1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而JDK1.8则是直接调用resize()扩容;
- 插入数据方式不同:插入键值对的put方法的区别,JDK1.8中会将节点插入到链表尾部,而JDK1.7中是采用头插,因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。;
- hash值计算不同:JDK1.7采用9次扰动(4次位运算+5次异或运算),JDK1.8采用2次扰动(1次位运算+1次异或运算)
- 扩容时JDK1.8会保持原链表的顺序,而JDK1.7会颠倒链表的顺序;而且JDK1.8是在元素插入后检测是否需要扩容,JDK1.7则是在元素插入前;
- 扩容后数据存储位置的计算方式不同:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
- 扩容策略不同:JDK1.7中是只要不小于阈值就直接扩容2倍;而JDK1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数大于6就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
二.JDK1.8put、get方法源码解读
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
1)put方法,直接调用putVal:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义存放元素的节点数组,节点,变量n,i;
Node<K,V>[] tab; Node<K,V> p; int n, i;
/*
步骤1:判断table是否为空
1)table 表示存储在Map中的元素的数组
2)(tab = table) == null 表示将table赋值给tab,并且判断tab是否为null。
3)(n = tab.length) == 0 表示,将tab的长度赋值给n,并判断n 是否等于-
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤2:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步骤3:节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤4:判断该链为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {// 步骤5:该链为链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度大于8转换为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 将链表转为红黑树
treeifyBin(tab, hash);
break;
}
// key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//要添加的元素和链表中存在的元素相等了,则跳出for循环,不需要再比较后面的元素了,直接进入下面的if语句去替换e的值
break;
// 说明新添加的元素和当前节点不相同,继续找下一个元素
p = e;
}
}
// e不为空,说明上面找到了一个去存储Key-Value的Node
if (e != null) {
//把e节点的值赋值给oldValue
V oldValue = e.value;
//根据条件判断是否替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//回调函数,把e节点移动到map最后
afterNodeAccess(e);
return oldValue;
}
}
// 统计数据改变次数
++modCount;
// 步骤6:超过最大容量就扩容
if (++size > threshold)
resize();
//回调函数,新节点插入之后回调,根据evict判断是否需要删除最老插入的节点
afterNodeInsertion(evict);
return null;
}
2)get方法:根据key的Hash找到具体的Node节点,直接返回node.value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode方法:获取目标key的Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断node数组已经初始化,根据key的hash找到first node
if ((tab = table) != null
&& (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 检查第一个Node是不是要找的Node
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同
return first;
// 遍历桶中元素
if ((e = first.next) != null) {
// 如果是红黑树,走红黑树遍历查找方式
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果是链表,则遍历链表看是否找到类似元素。
do {
// 依次遍历,和遍历桶顶元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
三.常见问题及参考回答
1.hashMap初始值的大小和负载因子的大小?
答:hashMap初始长度就是16,负载因子是0.75。hashMap所容纳的最大数据量为:长度*负载因子。即当长度达到这个值的时候就会发生扩容
2.hashMap线程安全方面会出现什么问题?
答:在JDK1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。在JDK1.8中,在多线程环境下,会发生数据覆盖的情况,所以多线程还是用concurrentHashMap。
3.当两个对象的 hashCode 相同会发生什么?
答: hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,碰撞发生,又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。
4.hash冲突的解决办法?
答:开放定址法、链地址法、再哈希法、建立公共溢出区。
5.为什么hashmap的在链表元素数量超过8时改为红黑树?为什么小于6又转为链表?
答:链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是o(n) ,红黑树基于二叉树的结构,查找元素的复杂度为o(logn),所以用红黑树存储可以提高搜索的效率。当链表长度为6时 :查询的平均长度为 n/2=3, 红黑树为 log(6)=2.6,当链表长度为8时 :链表 8/2=4,红黑树为log(8)=3
6.数组的长度为什么是2的次幂
答:hashMap在操作中,首先会对key进行hash得到哈希值,然后哈希值会与哈希数组长度取余得到一个索引值,即 h = hash % n,这个索引值h就是对应哈希桶数组的索引。&运算速度快,比%取模运算块,根据计算,Java的%、/操作比&慢10倍左右,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n。当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算 的时候,会出现重复的数据,会导致数组元素分布不均匀。
7.hashMap 和 hashTable 有什么区别?
答:hashMap是线程不安全的,hashTable 是线程安全的;hashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 hashTable 不允许;hashMap默认初始化数组的大小为16,hashTable默认初始化数组的大小为11,前者扩容时,扩大2n倍,后者扩大2n+1;hashMap需要重新计算hash值,而hashTable直接使用对象的hashCode。