当前位置: 代码迷 >> 综合 >> HashMap(jdk8)内部结构重点解析
  详细解决方案

HashMap(jdk8)内部结构重点解析

热度:18   发布时间:2023-12-15 06:52:24.0

1.关键参数

  • 默认初始容量 DEFAULT_INITIAL_CAPACITY = 1<<4 ,即16;
  • hashmap最大容积 MAXIMUM_CAPACITY = 1<<30;
  • 默认负载因子 DEFAULT_LOAD_FACTOR = 0.75;
  • 普通list容器转化为树形结构的 TREEIFY_THRESHOLD = 8,收缩时 UNTREEIFY_THRESHOLD = 6 ;
  • 最小的转为树形结构的容量 MIN_TREEIFY_CAPACITY = 64;

2.关键容器

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;final K key;V value;Node<K,V> next;......
}

3.resize扩容的源码分析


final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//将当前table暂存到oldtab来操作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;//阈值设置为Integer的最大值,好像是2147483647,远大于默认的最大容量return oldTab;//直接返回当前table,不用扩容}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 双倍扩大老内存和老阈值并赋给新的table}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {
                   //这种情况是初始化HashMap时啥参数都没加newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {
    //当只满足老阈值大于0的条件时,新阈值等于新容量*默认扩容因子float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;//把新的阈值赋给当前table@SuppressWarnings({
    "rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建容量为newCap的新tabletable = newTab;if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
    //对老table进行遍历Node<K,V> e;if ((e = oldTab[j]) != null) {
    //遍历到的赋给e进行暂存,同时将老table对应项赋值为nulloldTab[j] = null;if (e.next == null)//将为空的元素复制到新table中 <--------------------newTab[e.hash & (newCap - 1)] = e;//等于是创建一个新的空table然后重新进行元素的put,这里的table长度是原table的两倍else if (e instanceof TreeNode)//暂时没了解红黑树 <--------------------((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {
     // preserve order <--------------------Node<K,V> loHead = null, loTail = null;//用于暂存不需要移位的节点,loHead存的是链表的第一个阶段,即桶的第一个节点,loTail存的是包括loHead的整个链表Node<K,V> hiHead = null, hiTail = null;//用于暂存需要移位的节点,类似不需要移位的Node<K,V> next;do {
    next = e.next;if ((e.hash & oldCap) == 0) {
    //如果与的结果为0,表示不移位,将桶中的头结点添加到lohead和lotail中,往后如果桶中还有不移位的结点,就向tail继续添加if (loTail == null)//在后面遍历lohead和lotail保存到table中时,lohead用于保存头结点的位置,lotail用于判断是否到了末尾loHead = e;elseloTail.next = e;loTail = e;}else {
    //这是添加移位的结点,与不移位的类似if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {
    //把不移位的结点添加到对应的链表数组中去loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {
    //把移位的结点添加到对应的链表数组中去hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

重点分析(<--------------------标志出来的地方)

重点1

if (e.next == null)//将为空的元素复制到新table中 ,e.next ==null代表当前桶里面只有一个元素,直接把它取模放入新数组中,它仍然是链表的第一个元素newTab[e.hash & (newCap - 1)] = e;//等于是创建一个新的空table然后重新进行元素的put,这里的table长度是原table的两倍

重点2

else if (e instanceof TreeNode)//暂时没了解红黑树((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

重点3

else {
     // preserve orderNode<K,V> loHead = null, loTail = null;//用于暂存不需要移位的节点,loHead存的是链表的第一个阶段,即桶的第一个节点,loTail存的是包括loHead的整个链表Node<K,V> hiHead = null, hiTail = null;//用于暂存需要移位的节点,类似不需要移位的Node<K,V> next;do {
    next = e.next;if ((e.hash & oldCap) == 0) {
    //如果与的结果为0,表示不移位,将桶中的头结点添加到lohead和lotail中,往后如果桶中还有不移位的结点,就向tail继续添加if (loTail == null)//在后面遍历lohead和lotail保存到table中时,lohead用于保存头结点的位置,lotail用于判断是否到了末尾loHead = e;elseloTail.next = e;loTail = e;}else {
    //这是添加移位的结点,与不移位的类似if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {
    //把不移位的结点添加到对应的链表数组中去loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {
    //把移位的结点添加到对应的链表数组中去hiTail.next = null;newTab[j + oldCap] = hiHead;}}

重点4

if ((e.hash & oldCap) == 0) {
    if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;
}
else {
    if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;
}

问:为什么这里是e.hash & oldCap 而不是和前面那种类似取模运算的 (e.hash & oldcap-1) ?

答:举例 有一个hashcode = 01000110,00000110,00001110 ,原数组长度为16(1111),取模之后得到索引为1110,即14;
扩容后,hashcode & (0001 1111) ,结果实际看的是加粗标记的位,如果 e.hash & oldCap ==0,代表扩容后不需要移动;如果e.hash & oldCap == 1,则代表扩容后索引需要+扩容前数组长度;

4.个人自问自答

  1. 为什么hashmap中要用2的次幂来算?
    答:计算下标时取模运算很慢,用power of 2 可以用位与运算& 来更快优化性能。

  2. hashmap的key可以为null/
    答:可以为null,并且数组的第一个位置即索引为0的地方专门放null。

  3. 为什么jdk8中树形结构选用红黑树?
    答:因为不仅要保证查询效率,还要保证插入效率,选用红黑树是一个折中的考虑。

  4. 只要链表上的数量大于或者等于TREEIFY_THRESHOLD,就一定会转化为红黑树?
    答:不一定,代码里面还有个参数MIN_TREEIFY_CAPACITY,只有总的数量大于等于MIN_TREEIFY_CAPACITY 的时候才能变树,这也是整体上性能的考量。

  5. 为什么在小于7,转化为链表,大于7转化为红黑树。
    答:首先,红黑树不一定查询就比链表高效,当节点很多时,红黑树的效率较高;选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换;容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。

  相关解决方案