HashMap 调研
- 前言
- JDK1.8之前
- 拉链法:
- JDK1.8之后
- JDK1.7 VS JDK1.8 比较
- 优化了一下问题:
- HashMap的put方法的具体流程?
- HashMap的扩容resize操作怎么实现的?
前言
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除 困难;
链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各 自的优势,使用一种叫做拉链法
的方式可以解决哈希冲突
。
JDK1.8之前
JDK1.8之前采用的是拉链法。
拉链法:
将链表和数组相结合。也就是说创建一个链表数组,
数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将
链表转化为红黑树,以减少搜索时间。
JDK1.7 VS JDK1.8 比较
优化了一下问题:
- resize 扩容优化
- 引入了红黑树,目的是避免单条链表过长而影响查询效率
- 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
HashMap的put方法的具体流程?
public class HashMapDemo<K, V> extends HashMap<K, V> {// 默认初始容量 - 必须是 2 的幂。static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 如果任一带有参数的构造函数隐式指定更高的值,则使用最大容量。必须是 2 的幂 <= 1<<30。static final int MAXIMUM_CAPACITY = 1 << 30;// 初始因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 使用树而不是箱列表的箱计数阈值。当将元素添加到至少具有这么多节点的 bin 时,bin 会转换为树。该值必须大于2,并且至少应为8,以便与树木移除中有关收缩后转换回普通箱的假设相吻合。static final int TREEIFY_THRESHOLD = 8;// 在调整大小操作期间对(分割)bin 进行树形化的 bin 计数阈值。应小于 TREEIFY_THRESHOLD,且最多 6 个网格,以便在移除时进行收缩检测。static final int UNTREEIFY_THRESHOLD = 6;// bin 可以树化的最小表容量。 初始数量(否则,如果 bin 中的节点太多,则表的大小将被调整。)应至少为 4的倍数以避免调整大小和树化阈值之间的冲突。static final int MIN_TREEIFY_CAPACITY = 64;transient HashMapDemo.Node<K, V>[] table;transient Set<Entry<K, V>> entrySet;transient int size;transient int modCount;int threshold;// 创建一个节点Node类 作为链表使用static class Node<K, V> implements Map.Entry<K, V> {// hash值final int hash;// keyfinal K key;// 对应的值V value;// 子节点HashMapDemo.Node<K, V> next;Node(int hash, K key, V value, HashMapDemo.Node<K, V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}// get setpublic final K getKey() {return key;}public final V getValue() {return value;}public final String toString() {return key + "=" + value;}// 计算规则, 节点上的key 进行hashCode 计算,异或 hashCode 值public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}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;}}// 定义一个 hash 方法, 通过hashCode 得到一个长度 位运算 h>>> 高低16bitstatic final int hash(Object key) {int h;// key.hashCode()) ^ (h >>> 16) hashcode 和 自己hashcode 位运算异或return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}// put方法@Overridepublic V put(K key, V value) {return super.put(key, value);}V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {HashMapDemo.Node<K, V>[] tab;HashMapDemo.Node<K, V> p;int n;int i;// 初始化表大小或将表大小加倍。如果为空,则根据阈值字段中保存的初始容量目标进行分配if ((tab = table) == null || (n = tab.length) == 0) {// 步骤1:tab为空则创建 table未初始化或者长度为0,进行扩容n = (tab = resize()).length;}// 计算下标是否为空 通过hash算法 得到 p (n - 1) & hash 确定元素存放在哪个桶中if ((p = tab[i = (n - 1) & hash]) == null) {// 为空找不到,放到桶里面tab[i] = newNode(hash, key, value, null);}// 桶里面存在类else {HashMapDemo.Node<K, V> e;K k;// 步骤3:节点key存在,直接覆盖value比较桶中第一个元素(数组中的结点)的hash值相等,key相等if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {// 直接覆盖值e = p;}// 步骤4:判断该链为红黑树 hash值不相等,即key不相等;为红黑树结点 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node e可能为空else if (p instanceof TreeNode) {// 放入树中e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);}// 步骤5 不是树就是链表else {// 循环for (int binCount = 0; ; ++binCount) {// 为空最后一个节点if ((e = p.next) == null) {// 在链表最末插入Node结点p.next = newNode(hash, key, value, null);判断链表的长度是否达到转化红黑树的临界值,临界值为8// TREEIFY_THRESHOLD 属性 8 前面定义类if (binCount >= TREEIFY_THRESHOLD - 1)// 链表结构转树形结构treeifyBin(tab, hash);break;}// 判断链表中结点的key值与插入的元素的key值是否相等if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {// 相等,跳出循环break;}// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表p = e;}}// 判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的val ue这个值if (e != null) {// 记录e的valueV oldValue = e.value;// onlyIfAbsent为false或者旧值为nullif (!onlyIfAbsent || oldValue == null) {// 用新值替换旧值e.value = value;}// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}// 结构性修改++modCount;// 步骤6:超过最大容量就扩容 实际大小大于阈值则扩容if (++size > threshold) {// 插入后回调resize();}// node 节点插入afterNodeInsertion(evict);return null;}
}
-
判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
-
根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3;
-
判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals;
-
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值 对,否则转向5;
-
遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
-
插入成功后,判断实际存在的键值对数量size是否超多了 大容量threshold,如果超过,进行扩容。