HashMap1.8源码解析

HashMap1.8源码解析

首先看一下HashMap1.8的继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}

和1.7一样1.8不仅继承了AbstractMap,而且实现了Map、Cloneable和Serializable接口,所以HashMap也可以序列化。

HashMap1.8的存储结构
在1.7中,HashMap是以“数组+链表”的基本结构来存储key和value构成的Entry单元的。其中链表结构的存在是用来处理hash碰撞的。这种结构有它的优点,比如容易实现等。但是我们可以设想这样一种情况,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费o(n)o(n)的时间复杂度来进行查找。基于这点,1.8中将HashMap的基本结构进行了改善,其中hashMap的基本结构依然是“数组+链表”,但是当hash碰撞太多以至于链表过长的时候,链表结构将演化成树(具体来说应该是红黑树)的结构。我们都知道,红黑树是二叉查找树平衡形式的一种,因此查找性能较链表来说,有了很大提升。
在1.7中,是使用Entry这个类作为基本存储单元的,在1.8中,可能为了配合红黑树的使用,改进成了Node这个类,当然,差不多只是名字变了而已,类内部实现的形式差别不是很大。

可以通过下面这张图理解:
在这里插入图片描述
HashMap1.8的成员属性

    private static final long serialVersionUID = 362498820763181265L;

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,最小容量:16

    static final int MAXIMUM_CAPACITY = 1 << 30;//HashMap的最大容量
    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,这里和1.7中的原因是一样的,都是为了在时间和空间上做一个折中,选择最合适的负载因子以保证最优化

     //树的门阀值,即当链表的长度超过这个值的时候,进行链表到树结构的转变
    static final int TREEIFY_THRESHOLD = 8;

     //当低于这个值时,树变成链表
    static final int UNTREEIFY_THRESHOLD = 6;

     //下面这个值的意义是:位桶(bin)处的数据要采用红黑树结构进行存储时,整个Table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    //分配的时候,table的长度总是2的幂
    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    //总的KV数量
    transient int size;
    
     //这个值用于快速失败机制
    transient int modCount;
    //门限阈值,计算方法:容量*负载因子
    int threshold;

HashMap1.8的构造方法

	//	初始容量和加载因子
	public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
        	//	如果传入的数组的大小小于0,抛出异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
			//	如果传入的数组的大小最大值就是MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
            //	加载因子比0小,或者加载因子不是一个number,抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    //	只传输一个数组的大小,加载因子是默认的0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //	无参构造,加载因子是默认的0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    //	传递一个Map类型的m,加载因子还是默认的0.75
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

HashMap的put方法

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put方法直接调用了putVal方法

	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;
        //  现在的tab表示Node数组,n表示其长度
        if ((p = tab[i = (n - 1) & hash]) == null)
            //  执行到这里,表明当前的索引到的index下标,还未存放元素
            tab[i] = newNode(hash, key, value, null);
        else {
            //  执行到这里,表明当前index下标已经存放了元素
            Node<K,V> e; K k;
             //  检测要放的元素的key和已经存放在index下标的元素的key是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //  这里表明两个key是相等的,那么进行覆盖,替换原来的旧值
                e = p;
            else if (p instanceof TreeNode) //  若两个key不相等,那么检测当前下标所形成的非线性结构是否是红黑树?
                //  执行到这里,表明非线性结构是红黑树
                //  那就把它插入到红黑树里面
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //  执行到这里,表明存储结构是链表
                //  那就先对链表进行遍历,检测是否存过在这个key
                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    //  这里进行判断,是否当前链表的长度已经 >= 8
                            //  执行到这里,表明已经 >= 8,那么将这个链表转化成红黑树进行存储
                            //	当然,我们前面提到,转化成红黑树需要两个条件,这里只满足了其中之一
                            //	第二个条件在treeifyBin()函数里面进行判断
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))  // 这里判断是否存在key相等的节点
                        //    相等,跳出循环
                        break;
                    p = e;
                }
            }
            //  由于e!=null,那么就说明存在key,
            if (e != null) { // existing mapping for key
            //  这里进行旧值的替换
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
            //	开始执行子类覆盖节点之后的方法
                afterNodeAccess(e);
            //	因为是覆盖,所以长度并未增加,可以直接返回
                return oldValue;
            }
        }
        //  这个modCount是作为迭代器的总长度来用,
        ++modCount;
        //  先对hashmap的总容量进行+1,然后比较它和阈值的大小
        if (++size > threshold)
        //  已经比阈值大,那么进行扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在putVal方法中有这样一条判断:

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

实际上是hash值对(数字长度-1)取余(这里用位运算是因为位运算效率高)。
table的下标是通过将(数组长度-1)与hash值进行与运算得到的。在这个下标对应的位置进行存储。
得到这个下标,进行了一系列操作。目的是为了将它进行位扰动,从而增加散列度,减少哈希碰撞。

进行插入操作,分为三种情况:
1.插入位置无数据,直接存入
2.插入位置有数据,但是较少且符合链表结构存储的条件,那么以链表操作存入
3.插入位置有数据,但是以树结构进行存储,那么以树的相关操作进行存入
较1.7的put相比,复杂了很多,不过却换取了查找时的性能提升。

HashMap真正的初始化还是在put方法中进行的resize操作。

resize()方法

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //  判断table是否已经初始化过
        if (oldCap > 0) {
            //  oldCap > 0,说明table已经初始化过了
            //  判断旧的数组的容量是否已经达到或多于默认的最大容量(1 << 30)
            if (oldCap >= MAXIMUM_CAPACITY) {
                //  改变阈值为整型量的最大值:
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
           
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //  左移1位,表示扩大到原来的两倍
                newThr = oldThr << 1; // double threshold
        }
        //  判断旧的数组的阈值是否大于0
        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;
        //	从这里开始,是将旧的数组下的链,移到新的数组中去
        if (oldTab != null) {
        	//	开始循环遍历原来的数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //	如果当前的数组元素不为null,把值赋值给e
                if ((e = oldTab[j]) != null) {
                	//	把当前的数组元素赋值为null
                    oldTab[j] = null;
                    if (e.next == null)
                    	//	表明数组元素没后后继节点,该桶中只有一个节点
                    	//	将该节点放到新数组中(下标通过hash运算和长度-1相与得到)
                        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;
                            //	判断当前的bin在新数组中是否改变了位置
                           
                            if ((e.hash & oldCap) == 0) {
                            	//	结果为0,表明在新数组中的位置没有变动
                                if (loTail == null)
                                	//	当前的低位的头指向e所指向的空间,也就是链表的头部
                                    loHead = e;
                                else
                                	//	当前低位的尾的next指向e所指向的空间
                                    loTail.next = e;
                                    //	当前的低位的尾指向了e所指向的空间
                                loTail = e;
                                //	上述这几条语句,目的就是为了让头指针指向链表的头部,尾指针一直指向e所指向的空间
                            }
                            else {
                            //	这个else里面的语句的意思与上面if里面的意思一样,只不过是这个是存放在数组中下标高的链
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);//	这个do-while循环,目的就是进行链的遍历,并自行判断应该放在原来的位置还是新的位置。
                        //	然后下面这些语句是将这条链放在新的数组中,
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            //	执行完一次,继续循环执行,直到循环完旧的数组
            }
        }
        //	将新生成的数组进行返回,这也是等于是将旧的数组进行了扩容
        return newTab;
    }

树化操作treefyBin:

//对链表进行树结构的转化存储
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

关于HashMap中的tableSizeFor方法

 //返回根据给定的目标容量所计算出来的最接近的2的幂,这有利于改善hash算法
 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }

总结
1.8的HashMap源码较多,这里挑了一些讲下,其余的还是很好理解的。

  • 1.8中HashMap的基本结构还是以数组+链表的形式来存储的,链表没有达到树化的最小数量MIN_TREEIFY_CAPACITY,则进行扩容操作。满足树化的条件,则把链表的每个节点都转化为 TreeNode。通过TreeNode的treeify(Node<K,V>[] tab)方法构建树。
  • 源码要求HashMap底层实现数组的长度为2的幂,原因是可以得到较好的散列性能。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值