【JavaSE】【集合】HashMap1.8

参考博文:https://blog.csdn.net/v123411739/article/details/78996181

  • JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”;
  • JDK 1.8 的 HashMap 的数据结构如下图所示,当链表节点较少时仍然是以链表存在,当链表节点较多时会转为红黑树。
    在这里插入图片描述

1、常量和属性

// 默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
 
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;    
 
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
 
// 链表节点转换红黑树节点的阈值, 9个节点转
static final int TREEIFY_THRESHOLD = 8; 
 
// 红黑树节点转换链表节点的阈值, 6个节点转
static final int UNTREEIFY_THRESHOLD = 6;   
 
// 转红黑树时, table的最小长度
static final int MIN_TREEIFY_CAPACITY = 64; 

常量:

  • 默认容量16
  • 最大容量 1 << 30
  • 默认负载因子0.75
  • 树化阈值 8
  • 反树化阈值 6
  • 转红黑树数组最小长度 64

知识点: 树化阈值为什么为8?
根据泊松分布,链表长度达到8的概率很小;

知识点: 为什么反树化阈值为6和8不同?
防止在一个点上,反复树化和反树化


变量:

//1.HashMap底层真正的存储结构 Node数组
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;
 
transient int size ;
//2.记录HashMap结构变化次数
transient int modCount;

//3.阈值,当size达到此值,考虑扩容
//(capacity * load factor)
int threshold;

//4.加载因子  影响扩容频率
final float loadFactor;

2、存储结构

Jdk1.8的HashMap底层存储结构是数组+链表+红黑树;因此其内部维护了两个内部类,一个是Node,是链表的存储单元;一个是TreeNode,是红黑树的存储单元

2.1 链表节点Node

  • Jdk1.8的存储单元也是Map接口的内部接口Map.Entry的实现类;在Jdk1.7中,实现类叫做Entry,1.8 中改为Node;
  • Node从属性可以看出来是一个单向链表的节点
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V 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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        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;
        }
    }

2.2 红黑树节点 TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
   
    // ...
}

3、构造器

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }


    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

知识点:
无参构造器只做了一件事:给加载因子赋默认值0.75; 其他都是默认值;因此table为null、threshold为0

4、get 方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
 
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1.对table进行校验:table不为空 && table长度大于0 && 
    // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 6.找不到符合的返回空
    return null;
}

知识点: hashMap的get(key)方法是如何判断key是相同的?

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

key的hash值相同 并且 (key的地址相同 或者 key equals相同)

知识点: 如果没找到,返回null

5、put方法

5.1 hash(key)

在这里插入图片描述

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

知识点: Key的hash值的计算方式

  • 如果key为null, 那么hash值为0
  • 如果key不为null,(h = key.hashCode()) ^ (h >>> 16)

Jdk1.8hash算法的优势:由于index的求解方式是 将hash & table.length-1,这样高16位没有机会参与index值的计算,会增加hash冲突概率;为了降低冲突概率,将高16位加入到hash的计算

对比JDK1.7中求index的方式 : index=hash & table.length-1

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

5.2 空table进行初次resize()

在这里插入图片描述
当table是否为空或者length等于0,则调用resize方法对table进行初始化;


resize()源码:

    final Node<K,V>[] resize() {
        //没插入之前的数组oldTal
        Node<K,V>[] oldTab = table;
        //old的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //old的临界值
        int oldThr = threshold;
        //初始化new的长度和临界值
        int newCap, newThr = 0;
        //oldCap > 0也就是说不是首次初始化,因为hashMap用的是懒加载
        if (oldCap > 0) {
            //大于最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                //临界值为整数的最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //标记##,其它情况,扩容两倍,并且扩容后的长度要小于最大值,old长度也要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //临界值也扩容为old的临界值2倍
                newThr = oldThr << 1; 
        }
        /**如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,        
           如果是首次初始化,它的临界值则为0
        **/
        else if (oldThr > 0) 
            newCap = oldThr;
        //首次初始化,给与默认的值
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            //临界值等于容量*加载因子   16*0.75=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值
        if (newThr == 0) {
            //new的临界值
            float ft = (float)newCap * loadFactor;
            //判断是否new容量是否大于最大值,临界值是否大于最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了。
        threshold = newThr;
        //表示忽略该警告
        @SuppressWarnings({"rawtypes","unchecked"})
            //初始化
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //赋予当前的table
        table = newTab;
        //此处自然是把old中的元素,遍历到new中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //临时变量
                Node<K,V> e;
                //当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
                if ((e = oldTab[j]) != null) {
                    //把已经赋值之后的变量置位null,当然是为了好回收,释放内存
                    oldTab[j] = null;
                    //如果下标处的节点没有下一个元素
                    if (e.next == null)
                        //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
                        newTab[e.hash & (newCap - 1)] = e;
                    //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
                    else if (e instanceof TreeNode)
                        //把此树进行转移到newCap中
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.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;
                        }
                    }
                }
            }
        }
        //返回扩容后的hashMap
        return newTab;
    }

代码缩减:只看table==null || table.size() == 0时,resize()的有效代码:

 final Node<K,V>[] resize() {
        //oldTab = {}
        Node<K,V>[] oldTab = table;
        //oldCap = 0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //oldThr = 0
        int oldThr = threshold;
        
        int newCap, newThr = 0;
        // newCap = 16  
        newCap = DEFAULT_INITIAL_CAPACITY;
        //newThr =  16*0.75=12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 
        threshold = newThr;
   	    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
      
        return newTab;
    }

知识点: 对于一个空table,resize()干了两件事
1.创建一个容量为16的Node[] table
2.threshold赋予 16*0.75

5.3 table[index] == null

在这里插入图片描述

  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

知识点: index的算法
jdk1.8中,index的算法和1.7是一样的,都是(n - 1) & hash,这意味着HashMap1.8中数组的容量也始终为2n大小。

知识点: table[index] == null,直接将新节点放进去,其next指向null

5.4 table[index] != null

在这里插入图片描述
在这里插入图片描述
知识点1: 尾插法
jdk1.7是头插法,jdk1.8中,由于要判断链表是否需要树化,必须遍历链表获取链表的长度信息,因此顺手将1.7中的头插法改成了尾插法;

知识点2: 先插入后判断是否需要树化
当遍历到p.next==null了,也就是说链表后面已经没有节点了,此时先将新节点插入进去;再根据节点长度判断是否需要树化

知识点3:
新节点插入链表尾部,此时binCount还未加1,然后检查是否达到树化阈值 binCount >= 8 - 1。也就是说链表中的元素达到8个的时候,但是binCount 只有7 的时候就会执行treeifyBin()来判断是否需要树化

5.4.1 树化机制

在这里插入图片描述
在这里插入图片描述
知识点:

  • 当链表元素达到8的时候,会考虑是否树化;如果数组长度没达到64的时候不会树化,只会扩容;达到64了此时会将链表树化(树化机制:链表元素个数达到8个并且数组长度达到64时,链表进行树化)
5.4.2 扩容机制

这里看table不为空时如何resize()

final Node<K,V>[] resize() {
  
        Node<K,V>[] oldTab = table;
        //oldCap = 16
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //oldThr = 12
        int oldThr = threshold;

        int newCap, newThr = 0;
       
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //newCap = oldCap << 1   newThr = oldThr << 1 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        }
        //24
        threshold = newThr;
        //表示忽略该警告
        @SuppressWarnings({"rawtypes","unchecked"})
        
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //此处自然是把old中的元素,遍历到new中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //临时变量
                Node<K,V> e;
                //当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
                if ((e = oldTab[j]) != null) {
                    //把已经赋值之后的变量置位null,当然是为了好回收,释放内存
                    oldTab[j] = null;
                    //如果下标处的节点没有下一个元素
                    if (e.next == null)
                        //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
                        newTab[e.hash & (newCap - 1)] = e;
                    //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
                    else if (e instanceof TreeNode)
                        //把此树进行转移到newCap中
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.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;
                        }
                    }
                }
            }
        }
        //返回扩容后的hashMap
        return newTab;
    }

知识点: 不为空时,resize() 将table容量和threshold都扩大两倍;
也就是说期初是16 12 ,扩容为32 24,64 48

在这里插入图片描述

树化最小节点数

在这里插入图片描述
第一次扩容发生在添加第9个元素;此时容量变为32
第二次扩容发生在添加第十个元素,容量变为64
第三次扩容发生在添加第11个元素,此时树化

总结

(1)几个常量和变量值的作用:
①默认负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;
②负载因子final float loadFactor; 用于根据容量计算阈值
③阈值int threshold;
当size达到threshold阈值时,会扩容;
④树化阈值static final int TREEIFY_THRESHOLD = 8;
该阈值的作用是判断是否需要树化,树化的目的是为了提高查询效率;当某个链表的结点个数达到这个值时,可能会导致树化。
⑤树化最小容量值static final int MIN_TREEIFY_CAPACITY = 64;
当某个链表的结点个数达到8时,还要检查table的长度是否达到64,如果没有达到,先扩容解决冲突问题
⑥反树化阈值static final int UNTREEIFY_THRESHOLD = 6;
当删除了结点时,如果某棵红黑树的结点个数已经低于该值时,会把树重新变成链表,目的是减少复杂度。

(2)存储过程
A.先计算key的hash值,如果key是null,hash值就是0,如果为null,使用(h = key.hashCode()) ^ (h >>> 16)得到hash值;
B.如果table是空的,先初始化table数组;
C.通过hash值计算存储的索引位置index = hash & (table.length-1)
D.如果table[index]==null,那么直接创建一个Node结点存储到table[index]中即可
E.如果table[index]!=null,并且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),那么用新的value替换原来的value,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中。
F.如果table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key“相同”,那么用新的value替换原来的value,否则需要判断table[index]下结点个数,如果没有达到TREEIFY_THRESHOLD(8)个,那么(key,value)将会封装为一个Node结点直接链接到链表尾部。
G.如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个,那么再判断table.length是否达到MIN_TREEIFY_CAPACITY(64),如果没达到,那么先扩容,扩容会导致所有元素重新计算index,并调整位置;
H.如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个并table.length也已经达到MIN_TREEIFY_CAPACITY(64),那么会将该链表转成一棵自平衡的红黑树,并将结点链接到红黑树中。
I.如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。

思维导图

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值