Java集合系列-HashMap 1.8

Posted learun8080

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合系列-HashMap 1.8相关的知识,希望对你有一定的参考价值。

一、概述

HashMap是基于哈希实现的映射集合。
HashMap可以拥有null键和null值,但是null键只能有一个,null值不做限制。HashTable是不允许null键和值的。
HashMap是非线程安全的集合,HashTable是添加了同步功能的HashMap,是线程安全的。
HashMap是无序的,并不能保证其内部键值对的顺序。
HashMap提供了常量级复杂度的元素获取和添加操作(当然是在hash分散均匀的情况下)。
HashMap有两个影响功能的因素:初始容量与负载因子,当集合中的元素数量超过了初始容量和负载因子的乘积值时,会触发resize扩容
HashMap默认的初始容量是16,负载因子是0.75
HashMap在链表添加元素是采用尾插法,之前的版本采用头插法,因为会导致循环链表的问题,改成了尾插法,并添加了红黑树来优化链表过长的情况下查询慢的问题
HashMap底层结构为数组+链表/红黑树
HashMap底层链表的元素达到8个的情况下,如果HasnMap内部桶个数(即桶容量)达到64个则进行树形化,否则进行resize扩容

 

二、常量/变量解析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //...
    // 默认的初始容量,值为16,如果自定义也必须为2的次幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量值为2的30次幂,就是0100 0000 0000 0000 0000 0000 0000 0000 共32位
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的负载因子,值为0.75,可自定义,必须小于1
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 链表转树结构的元素数量界限值,当某个hash值下的链表元素个数达到8个,
    // 则将其改为树结构
    static final int TREEIFY_THRESHOLD = 8;
    // 红黑树反转链表的元素数量界限值,当数量小于6的时候才会反转
    static final int UNTREEIFY_THRESHOLD = 6;
    // 树形阈值,这个阈值针对的是整个Map中桶的数量,表示只有在所拥有的桶数量
    // 达到64时才能执行树形化,否则先去扩容去吧,可见在桶数小于64时,优先执行扩容
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 表示桶数组
    transient Node<K,V>[] table;
    // 缓存
    transient Set<Map.Entry<K,V>> entrySet;
    // 集合中元素的数量,
    transient int size;
    // 集合结构的修改次数,包括集合元素的增删,和集合结构的变化,仅仅更改已有
    // 元素的值并不会增加该值,主要用于Iterator的快速失败
    transient int modCount;
    // 集合的扩容阈值
    int threshold;
    // 集合的负载因子,默认0.75,时间与空间的折中,增加负载因子,
    // 能增加元素容纳量,减小空间消耗,却增加的查询的时间消耗,
    // 减小负载因子,能减少元素容纳量,减少查询时间消耗,但却要及早的去扩容,
    // 增加了空间消耗
    final float loadFactor;
    // ...

 

三、功能解析

3.1 添加元素操作

3.1.1 功能描述:

添加新的映射元素(newKey,newValue),首先通过特定的hash算法计算newKey的hash值(newHash)。

Hash算法:获取newKey的hashCode值,然后进行高低位相异或。
hashCode值的获取方法在Object类中已有定义,当然也有某些类进行了重写,总的来说有以下几种:

  • String类型的hashCode:自定义算法较复杂
  • 包装类型的hashCode:当前值
  • 其他类型的hashCode:类名[email protected]+内存位置的16进制表示

如果是首次添加元素,那么就意味着桶尚未初始化,所以这里会先执行初始化操作(resize),如果初始化成功或者非首次添加元素,那么开始定位元素的桶位。

桶定位算法:用之前hash算法的结果newHash与桶的个数-1进行与操作
该算法的本意是保留newHash值的后几进制位来确定桶位,如何保留后几位呢?我们知道二进制算法中1的与操作具有保留原值的效果
这里正是使用这种原理来实现,假设桶位数为16位,16的二进制位10000,16-1=15,15的二进制位就是1111,末四位全是1,通过1的
保留原值的作用,当那它与newHash值的二进制值进行与操作后,结果就是newHash保留后4位的结果,其余位置0。而4位正好在桶位0-15之内。
而这也就是桶位数必须是2的次幂的原因,因为2的次幂的数字的二进制值全部是首位为1,其后全是0的值,当其-1之后就会变成首位
变0,其后全是1的值,而桶的下标是从0开始,最高位正好是-1之后的值。

查看确认桶位是否已有元素,如果没有,直接存放新元素到该桶位,如果桶位已有元素存在,那么就是出现hash碰撞,这时的解决办法就是使用链表或者红黑树来存储,如果该桶位存储的数据结构已经是红黑树,那么执行红黑树添加元素操作,否则执行链表的尾插法,将新元素插入到链表的末尾。

尾插法:1.7之前的版本全是头插法,将新元素作为表头元素,1.8之后改成尾插法,将新元素作为表尾元素,至于原因就是为了避免多线程扩容导致循环链表出现
在执行尾插法的时候需要遍历链表,查找是否存在相同key的元素,若存在则直接用newValue替换旧值,不再执行插入操作。

新元素插入完成之后,校验Map中总元素个数是否达到了阈值(这的个阈值是桶容量和负载因子乘积),如果超过阈值则进行扩容。

3.1.2 源码解析:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //...
    public V put(K key, V value) 
        // 首先通过hash方法计算hash值,然后执行存值操作
        return putVal(hash(key), key, value, false, true);
    
    // hash算法:首先获取key的hashCode,然后将其高低16位相异或,全员参与(hashcode值的所有二进制位都能参与hash运算)
    static final int hash(Object key) 
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) 
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            // 首次添加值执行初始化
            n = (tab = resize()).length;
        // 定位桶下标,n的值为2的次幂,同时也是桶的数量,hash是之前通过hash算法得出的结果,n-1之后末几位全部是1,
        // 再和hash与运算,等于保留hash的后几位作为结果。比如:(1111)&(01010101)的结果为0101,保留了后四位   
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else 
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 存在相同key的情况(桶位置)
                e = p;
            else if (p instanceof TreeNode)
                // 红黑树的情况
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else 
                // 链表的情况
                for (int binCount = 0; ; ++binCount) 
                    // 遍历链表采用尾插法添加新元素
                    if ((e = p.next) == null) 
                        p.next = newNode(hash, key, value, null);
                        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))))
                        // 存在相同key的情况(链表元素位置)
                        break;
                    p = e;
                
            
            // 针对存在相同key的情况进行统一处理:替换value
            if (e != null)  // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            
        
        ++modCount;
        if (++size > threshold)
            resize();// 扩容两倍
        afterNodeInsertion(evict);
        return null;
        
    //...
    

 

注意:桶容量为2的次幂的原因正是因为便于元素通过位运算实现定位。

3.2 初始化/扩容操作

3.2.1 功能描述

执行扩容方法的原因主要是集合中元素数量达到阈值或者是集合桶数组某个桶位置的元素数量达到8个,
但集合桶容量未超过64的情况下,特殊的情况是首次添加元素时的初始化操作也走这个方法。

3.2.1.1 初始化

只会计算初始化容量和初始化阈值然后创建一个初始桶数组并返回结果。

对于使用了带参构造器的情况,会定制初始容量和负载因子,如果只定制了初始容量则使用默认负载因子,
构造器会通过一个进制运算根据自定义的容量算出一个大于等于自定义容量值的最小的2的次幂值作为真正的容量
比如:自定义容量为10,则计算容量值为16,然后再根据这个容量计算阈值为12。

3.2.1.2 扩容

首先校验旧容量是否已经达到或者大于容量最大值MAXIMUM_CAPACITY,如果是则不再进行扩容操作,还在原桶数组中保存元素,
并将阈值设置为Integer的最大值,设置为最大值之后就不会再触发扩容操作(因为Map中元素的总个数最大也就是Integer的最大值了,不可能比之更大),
然后校验容量加倍后的新容量是否超过容量最大值MAXIMUM_CAPACITY,如果没有的话则将阈值加倍。
新容量和新阈值都有了,然后创建新的桶数组,在之后就是元素迁移了。

3.2.1.3 元素迁移

遍历旧桶数组,校验每个桶位的元素结构,
如果只有一个元素,直接在新桶数组进行重定位,定位方式不变,
如果是红黑树,走树结构迁移逻辑,
否则就是链表,进行链表迁移,链表迁移进行了平衡优化,由于新桶数组和旧数组的两倍容量,
我们简单的将新容量分成相等的两半,称之为低位区与高位区,低位区下标与旧数组相同,
高位区下标为旧数组下标+旧数组容量。
链表迁移时,会根据该链表中元素的键的hash值与旧容量进行与运算,这就会有两个结果,为0或者不为0。

旧容量也是2的次幂,高位为1,其后全是0,比如10000(表示容量为16),将其和hash结果相与,
只会保留旧容量二进制为1的那一位对应的hash值的那一位,其余位全变成0,如果hash值的那一
位为0结果就是0,hash值那一位为1结果就是10000。

根据相与的结果来进行链表分拆,将结果为0的链表元素还定位到相同的桶下标位,即新桶数组的低位区,将结果为1的链表元素定位到原下标+旧桶容量的位置,即高位区。

这两个链表会先组装链表结构,然后将链表表头元素定位到低位区或高位区。

3.2.2 源码解析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //...
    final Node<K,V>[] resize() 
        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)
                // 如果加倍后的新容量没有超过最大容量,且旧容量大于等于16,则新阈值加倍
                newThr = oldThr << 1; // double threshold
        
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 只在通过带参数的构造器(初始容量和负载因子)
            // 创建的容器首次添加元素进行桶数组初始化时会走这里
            newCap = oldThr; 
        else                // zero initial threshold signifies using defaults
            // 初始化容量和阈值,这就是首次添加元素时执行的初始化逻辑
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        
        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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 创建桶数组
        table = newTab;
        // 如果是初始化操作,此处oldTab为null,会直接返回新建桶数组,否则执行元素迁移
        if (oldTab != null) 
            for (int j = 0; j < oldCap; ++j) 
                Node<K,V> e;
                if ((e = oldTab[j]) != null) 
                    oldTab[j] = null;
                    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  // preserve order 针对链表的情况
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do // 遍历旧桶位的旧链表
                            next = e.next;
                            // 这个判断的结果取决于在于hash值在于oldCap的1所在进制位对应的进制位是1还是0,
                            // 由于oldCap只有这一位为1,那么hash的该位将保留原值,其余位全部得0,增加这么
                            // 一个貌似随机的判断,用于进一步分散元素到不同的桶。
                            // 其实就是将旧桶第i位桶的链表元素分散到新桶的第i和第i+oldCap桶位上,为0还是为1随机
                            // 在循环中形成两个小链表,然后将首个元素赋值给新桶的对应桶位即可。
                            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;
                        
                    
                
            
        
        return newTab;
    
    //...

 

3.3 获取元素操作

3.3.1 操作描述

简单看代码

3.3.2 源码解析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //...
    public V get(Object key) 
        Node<K,V> e;
        // 计算key的hash值用作桶定位的基础
        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;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) 
            // 只有一个元素、链表头元素、树根元素要单独校验
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 存在链表或者树结构的情况
            if ((e = first.next) != null) 
                if (first instanceof TreeNode)
                    // 执行树结构获取元素逻辑
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 否则就是链表结构,遍历链表寻找匹配的元素
                do 
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                 while ((e = e.next) != null);
            
        
        return null;
        
    //...

 

3.4 移除元素操作

3.4.1 操作描述

简单看源码

3.4.2 源码解析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //...
    public V remove(Object key) 
        Node<K,V> e;
        // 计算key的hash值,用作后面定位元素桶位的基础
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) 
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 先进行桶定位,定位方式不变
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) 
            Node<K,V> node = null, e; K k; V v;
            // 针对只有一个元素、链表头元素、树根元素进行处理
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            // 如果第一个元素不是,针对链表和树结构进行后续元素处理
            else if ((e = p.next) != null) 
                if (p instanceof TreeNode)
                    // 执行树结构获取元素逻辑
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else 
                    // 链表循环获取等key的元素
                    do 
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) 
                            node = e;
                            break;
                        
                        p = e;
                     while ((e = e.next) != null);
                
            
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) 
                if (node instanceof TreeNode)
                    // 执行树结构删除元素操作
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    // 针对链表头和树根元素的情况
                    tab[index] = node.next;
                else
                    // 针对链表内元素进行删除
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            
        
        return null;
        
    //...

以上是关于Java集合系列-HashMap 1.8的主要内容,如果未能解决你的问题,请参考以下文章

java集合系列之HashMap源码

Java深入研究9HashMap源码解析(jdk 1.8)

Java集合系列五HashMap解析

Java入门系列之集合HashMap源码分析(十四)

JDK1.7&1.8源码对比分析集合HashMap

深入理解JAVA集合系列:HashMap源码解读