JDK8 HashMap源码解析—P2

299 阅读5分钟

  本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  上篇文章JDK8 HashMap源码解析—P1介绍了JDK8HashMap的成员属性、构造函数、putresize方法,今天接着介绍HashMap的其他方法。

常用方法

get方法

public V get(Object key) {
        Node<K,V> e;
        //调用getNode方法
        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;
        //判断索引位置处是否有值 否则直接return null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // hash值和key值都相等 则表明命中 直接返回
            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)
                    //是红黑树的话,调用getTreeNode方法查找
                    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;
    }

  get方法的流程如下:

  • 根据元素的hash值和hash(key)方法定位到数组中索引所在位置;

  • 判断该索引位置是否存在值:

    • 不存在的话,直接返回null

    • 比较hash值和key,若是相等,则表明命中,返回该索引位置所在的元素;

    • 若不相等,判断此处是否存在红黑树或者链表结构:

      • 若是红黑树,则调用getTreeNode方法进行查找;
      • 若是链表,则遍历链表进行查找;

  流程图如下所示:

image.png

JDK8中HashMap新增的getOrDefault方法

  JDK8中新增一个getOrDefault方法,该方法若是根据key查找不到对应的值value则返回传入的默认值。源码如下:


 public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        //可以看出与 get方法的不同 若是查找不到 默认返回defaultValue 其余与get方法一致
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }

remove方法


public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
    
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 {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            //node不为null 说明命中了要删除的节点
            // 如果不需要对比value 或者是需要对比value 但value也相等 则开始进行删除
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果是红黑树的话 调用removeTreeNode进行删除
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //如果是首节点的话 直接指向下一个节点 一个的话可以看做是只有一个节点的链表 这样也可以置
                //为null
                else if (node == p)
                    tab[index] = node.next;
                else
                    //否则的话,进行链表的移动
                    p.next = node.next;
                //更新modCount
                ++modCount;
                //更新size
                --size;
                afterNodeRemoval(node);
                //返回删除的节点
                return node;
            }
        }
        return null;
    }

  remove方法流程梳理如下:

  • 利用元素的hashhash(key)方法进行定位

  • 若是当前索引位置处为null,直接返回null

  • 比较hashkey,若相等,则表明找到了待删除的节点

  • 若不是,则判断该位置是红黑树还是链表:

    • 若是红黑树,则调用getTreeNode方法进行查找;
    • 若是链表,则遍历链表进行查找;
  • 若是找到了待删除节点,则开始进行删除:

    • 若该索引位置处为红黑树,则调用removeTreeNode方法进行删除;
    • 若是链表,则执行链表相关的操作进行删除;
  • 最后更新modCountsize等属性的值;

扩展

为什么数组长度都是2的倍数?

  • 当数组都是2的倍数时,2^n-1的二进制表示中所有位置都是1,这样与一个全部都是1的二进制数进行 & 操作时,速度会大大提升;
  • 计算元素的索引位置时,一般采用的是%操作,但是如果数组长度都是2的倍数的话,hash & (length-1) 等价于 hash % length,但是 & 操作的效率更高,因为 % 在操作系统会进行转换, & 操作不用;
  • 数组长度为2的倍数时,不同key计算出相同的index的概率较小,减少hash碰撞;

为了减少hash碰撞,hashMap做了哪些操作?

  • hash方法中,hashCode ^ hashCode >>>16,这样所得的hash值可以将hashCode的高位和低位都利用上,降低不同key通过hash方法获得相同hash值的概率,减少hash冲突;
  • 计算索引位置时,hash & (length-1),由于length始终是2的倍数,length-1的二进制表示中各位都是1,一个数与各个位都是1的数进行&操作,进一步降低hash冲突;

总结

  这里详细介绍了JDK8HashMapget方法和remove方法,可以看到JDK8中的getremove方法因为增加了红黑树这个数据结构,所以在查找的时候要考虑节点为红黑树的情况。最后补充了两个扩展知识,从这个知识点就能看出HashMap设计的精妙之处。总之,HashMap这个数据结构对Java程序员来说是必须掌握的知识点,希望大家结合上篇文章JDK8 HashMap源码解析—P1一起消化下,争取化为己用。(PS:再说一遍,写出HashMap的程序员们真的🐮)