JDK1.8 的新HashMap分析(转载)

JDK1.8 的新HashMap分析

 

一、综述

    Java中HashMap是非常常用的容器,因而也是需要面试官喜欢问的问题,难得有空,就对它进行源码级分析好了。

     一上来就看源码会有点头疼,那么先偷个懒,看看网上有没HashMap的分析。

 

图1 HashMap的结构图

一直到java1.7为止,HashMap的结构都是这么简单,基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。

这样子的HashMap性能上就将抱有一定疑问,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费0(N)的查找时间,这将是多么大的性能损失。这儿问题终于在1.8得到解决。当然解决的代价就是代码变得更加复杂orz….

      

图2jdk1.8hashmap结构图

到了1.8,当同一个hash值的节点数不小于8的时候,将不再以单链表形式存储了,会被调整成一棵红黑树(图中null节点没画)。这就是JDK1.8和1.7的最大区别。

好了接下来就按照HashMap的成员域、构造函数、put函数来分析下HashMap。

二、域

transientNode<K,V>[] table;

HashMap的散列表

 

transientSet<Map.Entry<K,V>> entrySet;

 

transient intsize;

记录HashMap中存储了多少个键值对<KEY-VALUE>

 

transient intmodCount;

mod是modify的缩写,hashMap的结构发生结构变化时会记录一次。

 

intthreshold;

当size大于这个数时,就进行一次扩容,即调用resize()函数

 

final floatloadFactor;

这是一个比例参数,当table中已经被占用的元素数与table总长度的比例不小于这个参数的时候,就会发生table的扩容,每次扩容都以2倍大小进行扩容,注意resize()函数

 

还有几个参数需要解释下:

static finalint DEFAULT_INITIAL_CAPACITY = 1<< 4;

默认初始化table的大小

static finalint MAXIMUM_CAPACITY = 1<< 30;

table的最大大小

 

static finalfloat DEFAULT_LOAD_FACTOR =0.75f;

默认loadFactor大小

 

static finalint TREEIFY_THRESHOLD = 8;

当节点冲突数达到8时,就会对hash表进行调整,如果table的长度小于64,那么会进行table扩容,如果不小于64,那么会将因冲突形成的单链表调整为红黑树。

 

static finalint UNTREEIFY_THRESHOLD = 6;

这个参数还不是很明白,有可能在删除冲突节点之后,可能同hash的节点数低于这个值时,将红黑树重新恢复为单链表。

 

static finalint MIN_TREEIFY_CAPACITY = 64;

注意到TREEIFY_THRESHOLD解释,不小于64时仅对table进行扩容,这个64就是指这个值。

三、构造函数

Node节点是对Key-Value的包装,是存储在HashMap中的节点,由于代码简单,就不做分析了。

    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(keye.getKey()) &&

                    Objects.equals(valuee.getValue()))

                    return true;

            }

            return false;

        }

    }

 

接着是HashTable的构造函数

    public HashMap(int initialCapacityfloat 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);

    }

对于HashMap,实际上有两个参数是可以由用户来设置的:

initialCapacity是初始化hash表的大小,但是HashMap中合法的Hash表大小是2的次幂,因此,会有tableSizeFor(int)函数进行数据调整,调整成不小于输入参数的最小的一个2的次幂数,比如输入3,那么输出就是4

LoadFactor就是table占用比门限,这个参数解释见第二节。

        HashMap还有几个构造函数,但比较简单,就不再解释

    public HashMap(int initialCapacity) {

        this(initialCapacityDEFAULT_LOAD_FACTOR);

    }

 

 

 

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(mfalse);

    }

四、put函数

public V put(K key, V value) {

        return putVal(hash(key), keyvaluefalsetrue);

    }

Put函数是这样的,和1.7差距有点大,因为1.7的时候put的所有代码都在put函数中,1.8的时候只是对putVal函数做了包装,因为要添加一个键值对,要更多参数了。

    final V putVal(int hash, K key, V valueboolean onlyIfAbsent,

                   boolean evict) {

        Node<K,V>[] tab; Node<K,V> pint ni;

        if ((tab = table) == null || (n = tab.length) == 0) //先判断,table大小,如果table为null,或者没分配空间,就resize一次

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

        if ((p = tab[i = (n - 1) & hash]) == null)  //如果首节点为null,就创建一个首节点。注意到tab[i = (n - 1) & hash],(n-1)&hash才是真正的hash值,也就是存储在table的位置(index)。

            tab[i] = newNode(hashkeyvaluenull);//创建一个新的节点

        else {//到这儿了,说明碰撞了,那么就要开始处理碰撞了

            Node<K,V> e; K k;//首先先去查找与待插入键值对key相同的Node,存储在e中,k是那个节点的key

            if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))//p这时候是指向table[i]的那个Node,这时候先判断下table[i]这个节点是不是和我们待插入节点有相同的hashkey值。如果是就e = p

                e = p;

            else if (p instanceof TreeNode)//到这儿,说明第一个节点的hashkey值与我们带插入Nodehashkey值不吻合,那么要从这个节点之后的链表节点或者树节点中查找。由于之前提到过,1.8HashMap存储碰撞节点时,有可能是用红黑树存储,那么先判断首节点p的类型,如果是TreeNode类型(Node的子类),那么就说明碰撞节点已经用红黑树存储,那么使用树的插入方法,如果新插入了树节点,那么e会等于null,用于后面的判断与处理

                e = ((TreeNode<K,V>)p).putTreeVal(thistabhashkeyvalue);

            else {//说明碰撞节点是单链表存储的

                for (int binCount = 0; ; ++binCount) {//单链表逐个向后查找

                    if ((e = p.next) == null) {//e引用下一个节点,如果是null,表示没有找到同hashkey的节点

                        p.next = newNode(hashkeyvaluenull);//创建一个新的节点,放到冲突链表的最后

                        if (binCount >= TREEIFY_THRESHOLD - 1) /注意到如果这时候冲突节点个数达到8个,那么就会treeifyBin(tab, hash)函数,看是否需要改变冲突节点的存储结构,这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。

                            treeifyBin(tabhash);

                        break;

                    }

                    if (e.hash == hash &&

                        ((k = e.key) == key || (key != null &&key.equals(k))))//如果找到了同hashkey的节点,那么直接退出循环

                        break;

                    p = e;//调整下p节点

                }

            }

            if (e != null) { //退出循环后,先看e是不是为null,为null表示添加了一个新节点,而不为null表示找到了一个与待插入同hash、同key的已存节点

                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null)//注意到这时候要判断是不是要修改已插入节点的value值,两个条件任意满足即修改

                    e.value = value;

                afterNodeAccess(e);//这个是空函数,可以由用户根据需要覆盖

                return oldValue;

            }

        }

        ++modCount;//当插入了新节点,才会运行到这儿,由于插入了新节点,整个HashMap的结构调整次数+1

        if (++size > threshold)//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容

            resize();

        afterNodeInsertion(evict); //这个是空函数,可以由用户根据需要覆盖

        return null;

    }

         这才是put函数的本体,先从输入参数开始解释:

Inthash:输入的hash值,是key的hash值,但是这个不是真正table的hash值,还得进行换算。

Kkey:键值对的键

Vvalue:键值对的值

booleanonlyIfAbsent:如果找到同key的键值对,是否更新value值,true就是更新,false就是不更新。

booleanevict:这个参数只在afterNodeInsertion(evict)函数中使用,但是这个函数是个空的函数,如果用户不覆盖这个函数,那么这个参数没有意义。

     好了,接下来就是分析源码了。不得不先吐槽一下,1.8的hashmap代码真的很丑啊。赋值语句强行放在if判断语句内,刚开始看还真是不习惯。

         好了,直接看上面源码的注释吧。

 

五、相关函数分析

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

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

        Node<K,V>[] oldTab = table;//临时变量赋值hash

        int oldCap = (oldTab == null) ? 0 : oldTab.length;//读取hash表的长度

        int oldThr = threshold;//读取hash表的扩容门限(对于节点个数)

        int newCapnewThr = 0;

        if (oldCap > 0) {//如果老table的容量进行判断

            if (oldCap >= MAXIMUM_CAPACITY) {//大于0的情况表示table已经存在,查看此容量大小,比MAXIMUM的话调整为MAXIMUM_CAPACITY

                threshold = Integer.MAX_VALUE;

                return oldTab;//返回老的表

            }

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

                     oldCap >= DEFAULT_INITIAL_CAPACITY)//扩容是将老容量大小*2

                newThr = oldThr << 1; // 调整门限也*2

        }

        else if (oldThr > 0) // 虽然老table容量为0,但是它的容量门限被谁都那个不是0,那么新的table容量就是OldThr

            newCap = oldThr;

        else {               // 如果老的table容量和门限都是0,用默认值进行初始化新table的容量和门限

            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;//HashMap的门限域被重现设置,前面都只是对临时变量进行操作的

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

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//使用新容量创建一个新表

        table = newTab;//HashMaptable域赋值新创建的table

        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {//table[0]~table[oldCap]进行节点转移

                Node<K,V> e;

                if ((e = oldTab[j]) != null) {//取出第一个节点就行,然后把oldTab[j]赋值为null

                    oldTab[j] = null;

                    if (e.next == null)

                        newTab[e.hash & (newCap - 1)] = e;// e没有后续节点,那么之前并没有发生碰撞,简单完成单节点转移就好了,由于新的table的容量变化了,那么e这个节点的它真正的hash值(存入table的位置index)是要重新计算的。

                    else if (e instanceof TreeNode)//如果e是树节点类型,说明这次移动的是一棵树

                        ((TreeNode<K,V>)e).split(thisnewTabjoldCap); //好了,这时候是对一棵树的调整

                    else { //到这儿e后面带着个单链表,那么就要逐个去将单链表中每个元素重新算在HashMap中的位置,并进行搬运

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

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

                        Node<K,V> next;

                        do {

                            next = e.next;//记录下一个节点

                            if ((e.hash & oldCap) == 0) {//原来的链表e因为新链表是2倍扩容,那么实际上会被拆分成两队,e.hash/oldCap为奇数一队,放在hi队中,而e.hash/oldCap为偶数一队,放在lo队中,这儿使用(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) {//lo队不为空,则放在新队的原位置

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

                        if (hiTail != null) {

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;//hi队不为空,则放在新队的[j+oldCap]

                        }

                    }

                }

            }

        }

        return newTab;

    }

 

final voidtreeifyBin(Node<K,V>[]tab, inthash)

    final void treeifyBin(Node<K,V>[] tabint hash) {

        int nindex; Node<K,V> e;

        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//table的长度还没达到MIN_TREEIFY_CAPACITY,那么要做的就是对table扩容,而不是链表变红黑树,通过2倍扩容,那么所有冲突形成的单链表会被切成两段,举例:原来同hash值得8个冲突节点形成的单链表,分成两个队列存储,原因见resize的分析

            resize();

        else if ((e = tab[index = (n - 1) & hash]) != null) {//到这儿,说明Hash表的长度至少是64了,那么要进行冲突节点单链表向红黑树变化

            TreeNode<K,V> hd = nulltl = null;

            do {//这个循环将单链表中的节点替换成TreeNode类型,并构建双向链表,这是为了重构红黑树做准备

                TreeNode<K,V> p = replacementTreeNode(enull);

                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本来就是为了提供高效率的查询,1.7以及之前的版本,使用单链表存储冲突节点,最坏的情况是存入n个节点全部冲突,当然由于resize扩容,那么HashMap的查询复杂度O(lgn),当然在新版本的HashMap中没当冲突节点个数大于8时,就先尝试table扩容,当table数达到64后,冲突节点数为8时,则进行链表向树结构转换,这样对于冲突节点的访问复杂度就会大幅度降低,当然这是建立在插入时冲突处理算法复杂度提升为代价的。

转载于  https://wenku.baidu.com/view/6e1035943968011ca30091cd.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值