HashMap在JDK1.7和1.8主要区别
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
首先从声明上来看,HashMap继承自AbstractMap 实现了Map、Cloneable、Serializable接口,点开AbstractMap 源码,发现AbstractMap 也实现了Map接口,那么HashMap为什么继承了AbstractMap 又要实现Map?完全无法解释的通,其实这就是类库设计者的写法错误。可以参考:http://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete回答。
HashMap在不同的JDK版本中底层的数据结构也不同,1.7的是数组+链表的实现方式,而1.8变成了数组+链表+红黑树的数据结构(当链表的长度大于8,转为红黑树)。
1.JDK1.7
简单描述一下HashMap的存值过程:
首先HashMap是数组+链表的数据结构。
- 当向HashMap中插入键值对的时候,首先会计算出key的hash值,然后根据hash值插入到数组相应的数组下标处。
- 一个数组元素=一个键值对=一个链表的头节点(hash,key,value,next)next表示下一个节点对象。
- 当数组下标中有元素的时候,则需要将原元素移动到链表中,冲突hash值对应的键值对放入数组元素中。(这和jdk1.8不同)
2.JDK1.8
此版本下HashMap的数据结构改变成数组+链表+红黑树(当链表长度大于8时,链表会转换为红黑树实现)。
为什么要引入红黑树呢:当链表的长度太长时,会影响HashMap的查询效率。时间复杂度O(n)。此时利用红黑树快速增删改查的特点将时间复杂度降为O(logn)
2.1 存储流程
2.2 实际存储对象
HashMap中数组的元素以及链表节点都是Node类实现与1.7相比(Entry)只不过是换了名字
/**
* Node = HashMap的内部类,实现了Map.Entry接口,本质是 = 一个映射(键值对)
* 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值,HashMap根据该值确定记录的位置
final K key; // key
V value; // value
Node<K,V> next;// 链表下一个节点
// 构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* equals()
* 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
*/
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
2.3 相关参数
/**
* 主要参数 同 JDK 1.7
* 即:容量、加载因子、扩容阈值(要求、范围均相同)
*/
// 1. 容量(capacity): 必须是2的幂 & <最大容量(2的30次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)
// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
final float loadFactor; // 实际加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子 = 0.75
// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;
// 4. 其他
transient Node<K,V>[] table; // 存储数据的Node类型 数组,长度 = 2的幂;数组的每个元素 = 1个单链表
transient int size;// HashMap的大小,即 HashMap中存储的键值对的数量
/**
* 与红黑树相关的参数
*/
// 1. 桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 2. 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 3. 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
// 否则,若桶内元素太多时,则直接扩容,而不是树形化
// 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
2.4 加载因子
2.5 hash计算原理
在将元素插入到HashMap集合中之前需要对元素进行hash计算,那么hash值的计算是如何得来的?以及为什么要使用这种方式来计算hash值?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
插入时首先判断key是否为null,(这表示hashmap可以存储null),如果不为null,先把hashcode值赋值给hash,然后将hash无符号位右移16位,在将结果和原来的值异或运算。
//h=key.hashCode()
0110 1101 0110 1111 0110 1110 0010 1000
//无符号右移16位,其实相当于把低位16位舍去,只保留高16位
0000 0000 0000 0000 0110 1101 0110 1111
//然后高16位和原 h进行异或运算
0110 1101 0110 1111 0110 1110 0010 1000
^
0000 0000 0000 0000 0110 1101 0110 1111
=
0110 1101 0110 1111 0000 0011 0100 0111
可以看到,其实相当于,我们把高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。
那么为什么这样会降低哈希碰撞的概率呢?
首先将元素放入数组中是通过对数组的长度取模运算,例数组的长度是16,那么18在数组中的位置应该为18%16=2,那么hashMap中put方法如何进行这样的一个过程?
//这是 put 方法中用来根据hash()值寻找在数组中的下标的逻辑,
//n为数组长度, hash为调用 hash()方法混合处理之后的hash值。
i = (n - 1) & hash
//18的2进制为
0001 0010
//(n-1)=15
0000 1111
//与运算
0000 0010=2
同样也是2,由于n是2的n次幂,所以n-1的二进制低位都是1,那么无论什么值与它进行与 运算最大值也只可能是n-1,最小值为0。因此,这个运算就可以实现取模运算,而且位运算还有个好处,就是速度比较快。
现在解释为什么要进行高低位异或运算来获取hash值
//如果直接通过原来的h与n-1进行&运算,并且此时有另外一个数h2 它的低位和h基本相同,但是高位有很大差异
h :0110 1101 0110 1111 0110 1110 0010 1000
h2 :0101 0110 1001 1010 0101 1101 0010 1000
n-1:0000 0000 0000 0000 0000 0000 0000 1111
//他们与n-1的&运算的值都是相同的,就是因为h和h2的高位特征完全 没有考虑进去,只有通过 (h = key.hashCode()) ^ (h >>> 16) 将高位和低位进行异或运算这样低位也保留了高位的特征,大大降低冲突的概率。
那么为什么要进行异或运算而不进行与运算、或运算?通过分析这几种运算的运算过程来解释:
与: 0 0 1 1 或: 0 0 1 1 异或: 0 0 1 1
与: 0 1 1 0 或: 0 1 1 0 异或: 0 1 1 0
值: 0 0 1 0 值: 0 1 1 1 结果: 0 1 0 1
值的比例(0:1)分别是 3:1(与) 1:3(或) 1:1(异或)
可以看出只有进行异或运算取值的比例才是最平衡的,所以,异或运算之后,可以让结果的随机性更大,而随机性大了之后,哈希碰撞的概率当然就更小了。
3.总结
- 底层的数据结构不一样:1.7数据结构为数组+链表的实现方式;1.8数据结构为数组+链表+红黑树的实现方式。
- JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7resize()方法只负责扩容,inflateTable()负责创建表。
- 1.7新增节点是采用头插法,而1.8是采用尾插法
- 在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。
图片转载自:https://blog.csdn.net/carson_ho/article/details/79373134
关于HashMap的源码详细分析:https://blog.csdn.net/carson_ho/article/details/79373134