javaSe——hashmap jdk1.8

hashmap jdk1.8

  • 1.实现的接口
  • 2.内部类
  • 3.hashmap内部的成员
  • 4.默认值 (注意)
  • 5.方法分析
    • 5.1构造方法
    • 5.2put方法
    • 5.3 get()
    • 5.4 remove()
  • 6.扩容+初始化哈希表 resize()
  • 总结

hashjdk1.7的实现看另一篇

1.实现的接口

在这里插入图片描述
jdk1.8的hashmap 和 1.7的一样都是实现一个map接口
javaSe——hashmap jdk1.8_第1张图片
包含了一些操作kv键值的一些常用的方法,get(),put(),remove()等等。
javaSe——hashmap jdk1.8_第2张图片
依然使用一个Set集合来保存所有的key,保证key的唯一性

2.内部类

javaSe——hashmap jdk1.8_第3张图片
迭代器就不介绍了。相较于jdk1.7,这里的出现了两个内部类,node和treeNode。

  • Node
    javaSe——hashmap jdk1.8_第4张图片
    点进去,这不就是jdk1.7版本中的entry嘛,这里只是修改了一下名字而已。功能也是一样的,用来保存每个kv键值对,next用于哈希冲突时,链接下一个Node。

  • TreeNode(红黑树节点)
    javaSe——hashmap jdk1.8_第5张图片
    这就是jdk1.8中引入的红黑树。分析成员,

  • parent 父节点

  • left 左子节点

  • right 右子节点

  • red 判断是否时红黑树

  • pre 指向链表的前一个节点

这里比较令人好奇的是红黑树中为什么会需要pre节点?这个不是用于双向链表中吗?
进入继承类中
javaSe——hashmap jdk1.8_第6张图片
在进入
javaSe——hashmap jdk1.8_第7张图片
到这里就知道答案了,TreeNode 内部隐式的继承了Node节点,因为node是单链表,只有next,这里存在一个pre,在内部隐式的构成了以双向链表。 —— 关于有什么作用后面在分析。

3.hashmap内部的成员

transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */ kv的set集合
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
     哈希表中实际的键值对的数量
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
     哈希表的修改次数 —— 用于迭代器时,方式在迭代的过程中删除哈希表中的数据做判断
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    阙值 —— 用于扩容的时的判断
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
     负载因子,用于计算阙值
    final float loadFactor;

分析可以知道,jdk8的成员和jdk7的基本是一致的。

4.默认值 (注意)

javaSe——hashmap jdk1.8_第8张图片
默认初始容量
最大容量
负载因子
上面的三个属性没有变化
javaSe——hashmap jdk1.8_第9张图片
但是在jdk8中多个3个默认的属性

  • TREEIFY_THRESHOLD = 8 表示树形的阙值
    就是哈希表中的某个槽位上的Node的数量大于8时,就会将其由链表变为红黑树

  • UNTREEIFY_THRESHOLD = 6
    取消树形化的阙值,红黑树种的节点的个数小于6时,就会将红黑树变为链表

  • MIN_TREEIFY_CAPACITY = 64
    树化时,哈希表的最小容量,因为树化完成之后,可能还会将红黑树转化为链表,转化过程种设计到槽位的重新计算,位置重新分配,保证哈希表种有足够的槽位

5.方法分析

5.1构造方法

javaSe——hashmap jdk1.8_第10张图片
可以看到依然是只有四个,所以这里就直接分析默认构造方法。
javaSe——hashmap jdk1.8_第11张图片
看到这里,忍不住笑了。jdkb做的比jdk7更绝,初始化时,就只是设置了一个默认的负载因子,jdk7好歹还设置了一下阙值,针对不同的实现,调用了一下init()方法。

然后看一下有参数的构造方法
javaSe——hashmap jdk1.8_第12张图片
参数值为初始化容量的大小,然后调用了另一个构成方法
javaSe——hashmap jdk1.8_第13张图片
这里就基本和jdk7一致,设置了一些基本的属性,包括阙值,负载因子,重点注意,这里依然没有对哈希表进行实例化(分配空间),真正的实例化过程在put()时才做。 好处就是,使用的懒加载机制,到正真使用哈希表时才初始化哈希表,提高空间利用率。

5.2put方法

javaSe——hashmap jdk1.8_第14张图片
不同于jdk1.7 直接在方法内部处理,这里是调用一个方法来处理put()。首先计算了一下hash值
javaSe——hashmap jdk1.8_第15张图片
计算的方式页没有1.7版本的复杂,直接调用了Object 对象的hashcod()函数得到哈希值
javaSe——hashmap jdk1.8_第16张图片
进入hashcode() 发现是一个native方法,所以可以知道计算hashcode值不是有java代码实现,而是c++ 实现。实现得原理是根据该对象在内存中得位置来计算得。 见下图
javaSe——hashmap jdk1.8_第17张图片
为什么不是用java 来实现,可能就是因为java操作内存比较慢,所以使用c++来实现。
计算得到得哈希值,然后向右逻辑循环右移了16位,使用高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;
         首先判断哈希表是否位null,在前面构造函数中并没有初始化哈希表,这里需要初始化哈希表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        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))))
                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))))
                        break;
                    p = e;
                }
            }
            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;
    }

代码做了一下几件事

  • 1.如果哈希表为空,就初始化哈希表

  • 2.根据哈希值计算槽位

  • 3.三个判断
    如果当前槽位为null,则直接将新的节点插入到对应槽位上
    如果当前节点为红黑树节点。则将该节点插入大该红黑树中
    如果该节点是链表节点,就将节点插入到链表中

  • 4.判断待插入的新节点是否存在,如果存在就将新节点的值插入到该节点中,然后返回旧节点的值

  • 5.modCount++ 操作的次数自增 —— 用于迭代器

  • 插入到红黑树节点分析

       for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }

可以看到整个插入的关键代码是在一个for中完成的。
javaSe——hashmap jdk1.8_第18张图片
这段代码主要就做了一件事,判断红黑树中是否存在该待插入的键,查找的过程是通过哈希值的大小来判断的,因为在存储的时候,使用的也是哈希值的大小关系来存储的,然后通过比较key得到判断的结果。
javaSe——hashmap jdk1.8_第19张图片
这段代码就是插入节点的真正的逻辑
分析可以知道:
这里在插入新节点时,在两个地方进行了插入,首先是将新节点祖父节点的下一个节点(链表),将新节点插入到红黑树的左节点或者右节点(红黑树)

注意图中划线的方法就是树结构的调节 —— 在红黑树原理篇中讲过树结构调节的原理。 代码有点复杂,懒得分析了(哈哈)。总之就是原理篇中讲过的各种情况,变色 + 旋转

  • 将节点插入到链表结构上
    javaSe——hashmap jdk1.8_第20张图片
    这里使用的是尾插法 (jdk1.7中使用的是头插法).
    javaSe——hashmap jdk1.8_第21张图片
    注意这段代码就是用来判断是否树型化**,就是当一个槽位上的单链表的节点的个数大于8时,在存入第9个节点的时候就需要进行树化,就是将单链表转化为双链表,然后转化为红黑树**。具体过程后面分析

到这了put()方法就分析完了,代码有点长,这里做个小结

首先是判断哈希表是否为null,根据情况创建哈希表,接着通过哈希值计算槽位值,然后是插入数据的三中情况,如果槽位值为null,则直接将新的节点插入,如果不为null,如果为红黑树,就使用红黑树的方式插入新的接待你,如果是链表结构,就使用链表的方式来插入新的节点。

5.3 get()

javaSe——hashmap jdk1.8_第22张图片
计算哈希值,然后进入getNode()
javaSe——hashmap jdk1.8_第23张图片
分析:通过哈希值计算槽位,判断槽位第一个元素是不是所需的节点,如果不是就判断第一个节点是红黑树还是链表节点,如果是红黑树,就使用查找红黑树的节点的方式来查找(查找过程类似于二叉搜索树),反之则使用循环遍历链表得到所需节点。

5.4 remove()

计算哈希值
javaSe——hashmap jdk1.8_第24张图片
删除方法分为两部分
首先是找到需要被删除的节点
javaSe——hashmap jdk1.8_第25张图片
寻找的方式也是根据节点的类型来查找(红黑树,链表)
javaSe——hashmap jdk1.8_第26张图片
这里的node节点就是找到待删除的节点。如果是树节点,则使用红黑树删除的方式来删除,如果是链表就使用链表的结构来删除

  • 红黑树删除节点
 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
                tab[index] = first = succ;
            else
                pred.next = succ;
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            if (root.parent != null)
                root = root.root();
            if (root == null
                || (movable
                    && (root.right == null
                        || (rl = root.left) == null
                        || rl.left == null))) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
                if ((p.right = sr) != null)
                    sr.parent = p;
                if ((s.left = pl) != null)
                    pl.parent = s;
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }

            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
        }

可以看到删除的代码是真的有点长,且不好理解,所以大致了解一下就可以了(感觉也记不住)。分析几个关键点

  • 取消树化
    javaSe——hashmap jdk1.8_第27张图片
    root 是根节点,当根节点为null,或者根节点的左节点或者右节点为null,或者左节点的左节点为null时就取消树化。

  • 调整 javaSe——hashmap jdk1.8_第28张图片
    在删除完节点后又重新进行了一次调整过程。

  • 链表删除节点
    这里的p表示的是node的父节点,删除时直接将父节点指向当前节点的下一个节点就好了。当前节点失去引用,等待垃圾GC回收。

6.扩容+初始化哈希表 resize()

分析方法得注释
javaSe——hashmap jdk1.8_第29张图片
该方法即可以用来初始化哈希表,也可以用来对哈希表进行扩容。注意区别:在jdk1.7中 初始化哈希表和扩容是两个不同得方法;
jdk1.7 初始胡哈希表
javaSe——hashmap jdk1.8_第30张图片

  • 扩容
   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)
                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;
        这里oldTable 为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;
                            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;
    }

代码有点长,分块来分析

首先是计算newCapacity,大小为原来的两倍,并创建新的哈希表
javaSe——hashmap jdk1.8_第31张图片
javaSe——hashmap jdk1.8_第32张图片
oldtable != null 使用来判断是初始化哈希表还是扩容
这里依然是分为链表和红黑树的不同情况单独进行扩容

  • 红黑树扩容
    具体扩容逻辑在split()中,代码分为两部分
    javaSe——hashmap jdk1.8_第33张图片
    注意这里参数传递过来的是新的哈希表。for循环中就是通过红黑树中的每个节点的哈希值逻辑于上一个bit值,得到高位链表节点和低位链表节点(关于为什么要分为高位节点和低位节点,我的理解就是,因为扩容后的哈希表的大小为原来的两倍,多出两倍的哈希槽位,所以可以将部分红黑树中才分为多个节点放入不同的槽位中,提高get的效率)

javaSe——hashmap jdk1.8_第34张图片
这里是取消树化的具体逻辑,
在这里插入图片描述
默认值为6,就是锁如果高位或者低位中的节点的个数 < 6 就会 进行取消树化
取消的逻辑
javaSe——hashmap jdk1.8_第35张图片
就是通过树型节点 创建一个新的链表节点,形成单链表

如果高位或者低位的节点的个数大于6,就会重新进行树化,如果高位和低位都大于6,那么就会形成两颗新的红黑树。

  • 链表扩容
    javaSe——hashmap jdk1.8_第36张图片
    可以看到在对链表进行扩容时,也是首先得到高低位两条单链表,就是将原来的一条单链表,通过计算得到 一条高位单链表和一条低位单链表。(注意每个槽位一个单链表)。
    存放
    javaSe——hashmap jdk1.8_第37张图片
    将两条链表存入新的哈希表的不同的位置

小结:
可以发现,在扩容时,无论时红黑树还是链表,操作方式都是类似的,首先将 红黑树或者链表,拆分为 高位链表 和 低位链表,然后再进行一变换 存入新的哈希表中。

总结

可以发现jdk1.8 版本的哈希表相对于1.7 真的是复杂了不少,但是其方法还是有规律。针对红黑树和链表 做了不同的处理,链表变为红黑树,红黑树变为链表 。

最后还有一个知识点: 就是jdk1.8 版本中是没有循环链出现的,但是也会有新的问题,就是值覆盖的问题。

你可能感兴趣的:(java,java)