写点什么

HashMap 详解,hadoop 源码分析完整版

用户头像
极客good
关注
发布于: 刚刚

[](

)构造函数


// 看一个参数比较全的构造函数,构造函数中并未给 table 分配内存空间,此构造函数 HashMap(Map<? extends K, ? extends V> m)会给 table 分配内存空间


public HashMap(int initialCapacity, float loadFactor) {


// 判断初始化容量是否合法,如果<0 则抛出异常


if (initialCapacity < 0)


throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);


// 判断 initialCapacity 是否大于 1<<30,如果大于则取 1<<30 = 2^30


if (initialCapacity > MAXIMUM_CAPACITY)


initialCapacity = MAXIMUM_CAPACITY;


// 判断负载因子是否合法,如果小于等于 0 或者 isNaN,loadFactor!=loadFactor,则抛出异常


if (loadFactor <= 0 || Float.isNaN(loadFactor))


throw new IllegalArgumentException("Illegal load factor: " +loadFactor);


// 赋值 loadFactor


this.loadFactor = loadFactor;


// 通过位运算将 threshold 设值为最接近 initialCapacity 的一个 2 的幂次方(这里非常重要)


this.threshold = tableSizeFor(initialCapacity);


}

[](

)hash 算法实现


HashMap 中的 hash 算法实现分为三步。其中第二步使用 hash 值高 16 位参与位运算,是为了保证在数组 table 的 length 比较小的时候,可以保证高低 bit 都参与到 hash 运算中,保证分配均匀的同时采用位运算,也不会有太多的性能消耗;其中第三步,当 n 是 2 的整数的幂次方是,hash&(n-1),相当于对 hash 值取模,而位运算比取模运算效率更高;具体流程可以通过图示查看。



第一步:通过 key.hashCode()获取 key 的 hashcode;



第二步:通过(h = key.hashCode()) ^ (h >>> 16)进行高 16 位的位运算;



第三步:通过(n - 1) & hash 对计算的 hash 值取模运算,得到节点插入的数组所在位置。


[](

)HashMap 之 put 方法


第一步:判断键值对数组 table[i]是否为空/null,是则执行 resize()扩容。



第二步:根据键 key 计算 hash 值得到插入数组的索引 i,如果 tab[i]== null 则直接插入,执行第六步;如果 tab[i] != null,执行第三步。



第三步:判断 tab[i]的第一个元素与插入元素 key 的 hashcode&equals 是否相等,相等则覆盖,否则执行第四步。



第四步:判断 tab[i]是否是红黑树节点 TreeNode,是则在红黑树中插入节点,否则执行第五步。



第五步:遍历 tab[i]判断链表是否大于 8,大于 8 则可能转成红黑树(数组需要大于 64),满足则在红黑树中插入节点;否则在链表中插入;在遍历链表的过程中如果存在 key 的 hashcode&equals 相等则替换即可。



第六步:插入成功,判断 hashmap 的 size 是否超过 threshold 的值,超过则扩容。



put(K key, V value)


public V put(K key, V value) {


// hash(key) 根据 key 计算一个 hash 值,具体实现如下函数


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


return putVal(hash(key), key, value, false, true);


}


计算 key 的 hash 值


static final int hash(Object key) {


int h;


// 如果 key 等于 null,返回 0


// 否则取 key 的 hashcode 值 h, 将 h 无符号右移 16 位也就是获取高 16 位,将两个值做异或位运算后返回


return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);


}


putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {


Node<K,V>[] tab; Node<K,V> p; int n, i;


// 判断 table 是否为空,为空则 resize()创建


if ((tab = table) == null || (n = tab.length) == 0)


n = (tab = resize()).length;


// (n - 1) & hash 计算插入节点在数组中的索引 index,为空则直接插入


if ((p = tab[i = (n - 1) & hash]) == null)


tab[i] = newNode(hash, key, value, null);


else {


Node<K,V> e; K k;


// 如果节点 key 存在,则覆盖当前元素


if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))


e = p;


// 节点不存在且当前的数组节点上的链为 RBT 红黑树,则按照红黑树的方式插入节点


else if (p instanceof TreeNode)


e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);


// 链为链表


else {


for (int binCount = 0; ; ++binCount) {


// 节点的下一个节点为 null,说明遍历到了链表的最后一个节点,将当前遍历到的最后一个节点的 next 指向新插入节点


if ((e = p.next) == null) {


p.next = newNode(hash, key, value, null);


// 链表长度大于 8 可能需要做红黑树转换,具体要看 treeifyBin()中判断数组的长度是否大于 64


if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st


treeifyBin(tab, hash);


break;


}


// 节点存在则返回


if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))


break;


// 不存在且写一个节点不为 null,则将下一个节点赋值给当前节点,继续下一次循环


p = e;


}


}


// 节点存在替换后直接返回,不会记录 HashmMap 结构变化


if (e != null) { // existing mapping for key


V oldValue = e.value;


if (!onlyIfAbsent || oldValue == null)


e.value = value;


afterNodeAccess(e);


return oldValue;


}


}


// 记录 HashMap 的结构变化


++modCount;


// 判断节点插入后是否需要对 hashMap 的数组进行扩容


if (++size > threshold)


resize();


afterNodeInsertion(evict);


return null;


}


扩容


final Node<K,V>[] resize() {


// 将扩容前的数组赋值给 oldTab


Node<K,V>[] oldTab = table;


int oldCap = (oldTab == null) ? 0 : oldTab.length;


int oldThr = threshold;


int newCap, newThr = 0;


if (oldCap > 0) {


// 判断数组长度是否大于等于允许的最大值,超过则取最大值,直接返回


if (oldCap >= MAXIMUM_CAPACITY) {


threshold = Integer.MAX_VALUE;


return oldTab;


}


// 将原先数组大小左移一位,也就是扩大两倍,扩容前需要判断大小是否超过允许的最大值


else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&


oldCap >= DEFAULT_INITIAL_CAPACITY)


newThr = oldThr << 1; // double threshold


}


// 如果数组大小小于等于 0 且 threshold 大于 0(比如 HashMap 中的元素被全部删除了),则将 oldThr 的值赋值给 newCap


else if (oldThr > 0)


newCap = oldThr;


//首次初始化,给与默认的值


else {


newCap = DEFAULT_INITIAL_CAPACITY;


newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);


}


// 如果新的扩容临界值仍为 0,需要给 newThr 计算一个值


if (newThr == 0) {


float ft = (float)newCap * loadFactor;


newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?


(int)ft : Integer.MAX_VALUE);


}


// 赋值新的数组扩容阈值


threshold = newThr;


@SuppressWarnings({"rawtypes","unchecked"})


// 根据新的容量实例化一个新的 Node 数组


Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];


table = newTab;


if (oldTab != null) {


// 遍历旧数组赋值到新数组中


for (int j = 0; j < oldCap; ++j) {


Node<K,V> e;


// 如果节点不为 null,则将节点赋值给 e


if ((e = oldTab[j]) != null) {


// 将节点引用置空,等待 GC 回收


oldTab[j] = null;


// 如果节点的 next 指向为空,则根据节点记录的 hash 值 &扩容的大小-1,重新计算节点在数组中的索引位置


if (e.next == null)


newTab[e.hash & (newCap - 1)] = e;


// 如果节点是红黑树节点,按照红黑树节点插入方式插入


else if (e instanceof TreeNode)


((TreeNode<K,V>)e).split(this, newTab, j, oldCap);


// 此处表示当前节点上的链为链表,遍历链表,将链表转移到新的数组上


else {


// 创建两条链 lo 链在原先数组节点位置


Node<K,V> loHead = null, loTail = null;


// hi 链插入原先数组索引+oldCap 位置


Node<K,V> hiHead = null, hiTail = null;


Node<K,V> next;


do {


next = e.next;


// 重点


// 获取当前节点的 hash 值,与 oldCap 做按位与运算,如果运算结果为 0,则加入 lo 链


if ((e.hash & oldCap) == 0) {


if (loTail == null)


loHead = e;


else


loTail.next = e;


loTail = e;


}


// 否则加入 hi 链


else {


if (hiTail == null)


hiHead = e;


else


hiTail.next = e;


hiTail = e;


}


} while ((e = next) != null);


// 新数组的原位置指向 lo 链的头节点


if (loTail != null) {


loTail.next = null;


newTab[j] = loHead;


}


// 新数组的 j+oldCap 指向 hi 链的头节点


if (hiTail != null) {


hiTail.next = null;


newTab[j + oldCap] = hiHead;


}


}


}


}


}


return newTab;


}

[](

)图文聊聊 jdk1.8 中扩容后 resize()那些事


在 jdk1.8 中,当发生 HashMap 扩容时我们通过源码可以发现,HashMap 并未重新通过新的数组长度来确定节点的位置,而是通过(e.hash & oldCap) == 0,原 hash 值与原数组长度做与位运算来确定节点在新数组中的位置,当(e.hash & oldCap) == 0 时,节点在新数组中的位置和老数组位置一致;当(e.hash & oldCap) != 0 时,节点在新数组中的位置为 j + oldCap,也就是原先的位置加上老数组的长度。



这里抛出两个问题,带着问题来分析



问题一:为什么能这么实现呢?



问题二:以前 hash 值相同的节点在数组的同一个索引位置上,现在竟然会被随机分配到两个新的索引位置上,这会不会导致 get(key)时无法获取到元素呢?

[](

)三个前提条件


1、HashMap 在做初始化的时候数组的默认大小是 16,且如果我们设值的初始值不是 2 的整数次幂,那么它会自动的去计算一个与初始化值最靠近的 2 的整数次幂的初始值,具体实现可以查看源码中的 tableSizeFor()方法。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
HashMap详解,hadoop源码分析完整版