总览
- JDK1.7和JDK1.8的区别
- 属性解释
put()
过程解析- 计算
threshold
- JDK1.8扩容优化
- JDK1.7死循环图解
区别
JDK1.7
- 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
- 当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,极端情况HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
- 数组形成链表,新的节点添加在头节点(会有死循环)
JDK1.8
- JDK7与JDK8中HashMap实现的最大区别就是对于冲突的处理方法。JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn)),用数组+链表+红黑树的结构来优化这个问题。
- 数组形成链表,新的节点添加在尾节点
resize()
不需要重新rehash()寻址- 解决了
resize()
时多线程死循环问题,但仍是非线程安全的 - 数组形成链表,新的节点添加在尾节点
属性解释
属性定义
transient Node<K,V>[] table
HashMap的哈希桶数组,非常重要的存储结构,用于存放表示键值对数据的Node元素transient Set<Map.Entry<K,V>> entrySet
HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能transient int size
HashMap中实际存在的Node数量,注意这个数量不等于table的长度,甚至可能大于它,因为在table的每个节点上是一个链表(或RBT)结构,可能不止有一个Node元素存在transient int modCount
HashMap的数据被修改的次数,这个变量用于迭代过程中的Fail-Fast机制,其存在的意义在于保证发生了线程安全问题时,能及时的发现(操作前备份的count和当前modCount不相等)并抛出异常终止操final float loadFactor
也是加载因子,衡量HashMap满的程度,当实际大小超过临界值时,会进行扩容,默认0.75int threshold
达到临界值,当元素达到临界值会进行扩容2倍,threshold = 加载因子*容量
默认属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
默认初始大小16static final float DEFAULT_LOAD_FACTOR = 0.75f
默认扩容是按照原容量的0.75倍进行扩容static final int TREEIFY_THRESHOLD = 8
当某个桶节点大于8,且总数超过64,转化为红黑树,否则扩容static final int UNTREEIFY_THRESHOLD = 6
当某个桶节点小于6时,会转化为链表,前提它是红黑树static final int MIN_TREEIFY_CAPACITY = 64
当整个HashMap中的元素数量大于64时,且某个桶节点大于8,也会转化为红黑树结构static final int MAXIMUM_CAPACITY = 1 << 30
最大容量
put()解析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
发生冲突时,链表中新节点jdk1.7中是放在首位,jdk1.8是放在尾节点
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, //这里onlyIfAbsent表示只有在该key对应原来的value为null的时候才插入,也就是说如果value之前存在了,就不会被新put的元素覆盖。
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //定义变量tab是将要操作的Node数组引用,p表示tab上的某Node节点,n为tab的长度,i为tab的下标。
// 将成员变量 table 赋值给本地变量 tab,并且将tab的长度赋值给本地变量 n
// 如果tab为空或者 数组长度为0,进行初始化,调用 resize()方法,并且获取赋值后的数组长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else