欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java源码解读系列13—HashMap(JDK1.8 )

程序员文章站 2022-06-04 18:57:34
...

1 概述

  • JDK7的HashMap底层采用数组+链表数据结构进行存储,会存在一个问题,即当解决哈希碰撞的链表过长的时候,HashMap的查询效率就会变低。红黑树是平衡二叉树(黑色平衡),其查询效率为O(lgn),优于链表的查询效率O(n)。 因此JDK8的HashMap底层采用数组+链表+红黑树数据结构进行存储。
  • 具体过程如下, 1)每次新增一个数据,就会被插入到数组中的空位。慢慢的数据多了,就会存在哈希碰撞问题,即2个key的索引一样。HashMap采用的解决方法是链地址法。数组中每个元素设置链表。key映射到数组某个元素中,而数据项本身插入到元素的链表中。2)当链表中的节点个数大于8时,链表会转变为红黑树3)当链表的节点个数小于等于6时,红黑树又会转变为链表
  • JDK8的HashMap中使用大量红黑树的方法,本文重点放在HashMap本身,具体红黑树的增删查改以及红黑平衡等特性均不做深入探讨。

2 构造函数

2.1 无参构造函数

JDK7的HashMap的无参构造函数初始化时候,会给存储的数组初始化并赋予默认大小16。JDK8已经不将存储的数组的大小初始化为默认值16,而是留到第一次put操作时对存储数组大小扩容到默认值16.

  public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

2.2 有参构造函数

这里虽然指定了初始化容量,但是初始化后存储数组的大小依然为0,依然留到第一次put操作的时候才会进行扩容。但这里会改变数组扩容的阀值threshold为大于initialCapacity的最小2的n次方。

  public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

public HashMap(int initialCapacity, float 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);
    }  

3 节点类型

3.1 链表的节点

Node类型是构成链表的节点

 static class Node<K,V> implements Map.Entry<K,V> {
        //哈希值
        final int hash;
        //存储K-V结构的key值
        final K key;
        //存储K-V结构的value值
        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;
        }

           ...
    }

3.2 红黑树的节点

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 
        //父节点
        TreeNode<K,V> parent;  // red-black tree links
        //左子节点
        TreeNode<K,V> left;
        //右子节点
        TreeNode<K,V> right;
        //前驱节点
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        //节点颜色是否为红色   
        boolean red;
        
        /有参构造函数
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        ...
 }

4 添加数据

4.1 put方法

直接调用putVal方法

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

4.2 hash方法

获取key的哈希值

 static final int hash(Object key) {
        int h;
        //h >>> 16:无符号右移动16位
        //h和h >>> 16做了异或操作
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

4.3 putVal方法

通过 (n - 1) & hash定位到新增的key在数组中的哪个位置上

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断table数组是为为空或者长度为0
        if ((tab = table) == null || (n = tab.length) == 0)
             //对table进行扩容
            n = (tab = resize()).length;
         //判断待插入位置上的节点p是否为空
        if ((p = tab[i = (n - 1) & hash]) == null)
           //创建一个新的节点
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //判断待插入位置上的节点p的key是否和待插入的key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //将p赋值给e
                e = p;
                //判断待插入位置上的节点p是否为类TreeNode的一个实例
            else if (p instanceof TreeNode)
               //调用红黑树的putTreeVal方法添加数据
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //无限循环,直到找到匹配条件
                for (int binCount = 0; ; ++binCount) {
                   //寻找p的下一个为空的节点
                    if ((e = p.next) == null) {
                       //将p的next引用指向新创建的节点
                        p.next = newNode(hash, key, value, null);                        
                        //树化的条件之一是链表的节点数  >  TREEIFY_THRESHOLD
                        //这里为什么TREEIFY_THRESHOLD要减去1?这是因为binCount为0时,其实已经是链表的第2个节点。也就是说满足binCount >= TREEIFY_THRESHOLD - 1 = 8 -1 = 7的条件时,链表的节点数至少为9个
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                           //调用红黑树的树化方法
                            treeifyBin(tab, hash);
                        //跳出循环
                        break;
                    }
                     //p的下一个节点e非空,则判断e的key是否和待插入的key相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //跳出循环
                        break;
                    //将e赋值给p,走到这一步说明又要开始遍历for循环
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                  //钩子方法,留给实现类做拓展,比如LinkedHashMap。
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //更新修改次数
        ++modCount;
        //执行添加操作后,HashMap中节点数size执行加1操作
        //如果size超过阀值threshold,调用resize()方法扩容
        if (++size > threshold)
            resize();
            //钩子方法
        afterNodeInsertion(evict);
        //返回空
        return null;
    }

4.4 treeifyBin方法

索引位置为 (n - 1) & hash上的链表的节点数大于8且数组的容量大于64时,会将链表转换为红黑色。这里需要注意的是不是将数组上所有的元素都转换为红黑树,而是仅仅转换索引位置为 (n - 1) & hash上的链表为红黑树。

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //数组为空或者数组的容量小于64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //扩容
            resize();
         //判断索引位置为index = (n - 1) & hash上的元素是否为空
        else if ((e = tab[index = (n - 1) & hash]) != null) {
             //使用一个链表存储红黑树的节点,其中hd指向链表头,tl指向链表尾节点
            TreeNode<K,V> hd = null, tl = null;
            do {
               //调用replacementTreeNode,通过e节点新生产一个红黑树节点p
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //首次遍历,尾节点为空
                if (tl == null)
                    //将hd指向p
                    hd = p;
                else {
                   //保留p上一个节点的信息
                   //将p的头引用指向tl
                    p.prev = tl;
                    //将tl的下一个引用指向p
                    tl.next = p;
                }
                //将尾节点的引用tl指向p
                tl = p;
              //判断e的下一个节点是否为空
            } while ((e = e.next) != null);
            
            //将hd赋值给数组index位置上的元素
            //判断hd是否为空
            //若不为空,因为hd -> .. ->tl目前是通过链表的形式保存上下节点的信息,调用treeify方法将链表转换为红黑树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

4.5 replacementTreeNode方法

使用链表类型的p节点构建一个红黑树节点

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

5 查询

5.1 get方法

通过key查询数据,直接调用getNode方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5.2 getNode方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //判断数组是否为空,数组长度是否为0,数组(n - 1) & hash位置上的元素first是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
         //判断first的key是否和待获取的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) {
            //判断first节点是否为红黑树节点类型的一个实例
            if (first instanceof TreeNode)
               //调用红黑树getTreeNode方法进行查找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                 //first节点是链表类型的一个实例
                //循环遍历找到某元素的哈希值和key与待获取的哈希值和key都相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //找不到与待获取的key相等的元素,返回空
    return null;
}    

6 删除

6.1 remove方法

remove方法删除元素实际调用removeNode方法

 public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

6.2 removeNode方法

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;
           //判断数组是否为空,数组长度是否为大于0,数组(n - 1) & hash位置上的元素p 是否为空
        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;
             //判断p的哈希值是否与待删除的hash值相
             //判断p的key是否和待删除的key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //将p赋值给node
                node = p;
            //判断p的下一个节点e是否为空
            else if ((e = p.next) != null) {
               //判断p是否为红黑树节点类型TreeNode的一个实例
                if (p instanceof TreeNode)
                   //调用红黑树的getTreeNode方法进行查找
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                          //p节点是链表类型的一个实例
                         //循环遍历找到某元素的哈希值和key与待删除的哈希值和key都相等
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                             //找到符合条件的节点
                            node = e;
                            //跳出循环
                            break;
                        }
                        //将e赋值给p,主要是了保存待删除的节点e的上一个节点的信息
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            
            //待删除节点node不为空
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                  // 判断node是否为红黑树节点类型TreeNode的一个实例
                if (node instanceof TreeNode)
                    //调用红黑树的removeTreeNode方法进行删除    
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
               
               //node节点是链表类型的一个实例  
               //node等于p,说明node位于链表的首位
                else if (node == p)
                   //将node的下一个元素赋值给数组index位置
                    tab[index] = node.next;
                else
                     //node不为链表的首位,将node的上一个节点的引用指向node的下一个节点,是否node节点             
                    p.next = node.next;
                //修改次数加1
                ++modCount;
                //节点总数减1
                --size;
                //钩子方法
                afterNodeRemoval(node);
                //返回node
                return node;
            }
        }
        //找不到待删除元素,返回空
        return null;
    }    
    

7 扩容

JDK7的HashMap的无参构造函数初始化时候,会给存储的数组初始化并赋予默认的的大小。JDK8是在首次添加的时,通过resize方法给将数组的扩容到大小到16。

7.1 resize方法

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //旧的数组容量,首次进来时oldTab为空,即oldCap为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧的扩容阀值
        int oldThr = threshold;
        //新的数组容量和扩容阀值
        int newCap, newThr = 0;
        //判断旧的数组容量是否大于0
        if (oldCap > 0) {
            //旧的数组容量大于等于MAXIMUM_CAPACITY
            if (oldCap >= MAXIMUM_CAPACITY) {
                //将扩容阀值设为0x7fffffff
                threshold = Integer.MAX_VALUE;
                //返回旧的数组
                return oldTab;
            }
            //新的数组容量为旧的左移一位
            //新的数组容量小于MAXIMUM_CAPACITY且旧的数组容量大于等于DEFAULT_INITIAL_CAPACITY
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                 //新的扩容阀值设为旧的2倍
                newThr = oldThr << 1; // double threshold
        }
        //旧的扩容阀值大于0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //使用带指定初始化容量的构造函数,首次调用resize方法时,oldThr不为0但是oldCap为0
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 使用无参构造函数,首次调用resize方法时oldThr和oldCap的值都为0
            //newCap和newThr重新赋值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //新的容量为0时
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            //判断newCap和ft是否小于MAXIMUM_CAPACITY
            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是否为空,若不为空,则将旧数组中的值复制到新的数组中
        if (oldTab != null) {
           //遍历旧的数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判断oldTab[j]是否为空,不为空则进行复制
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果e.next为空,说明该位置的链表上只有一个元素
                    if (e.next == null)
                        //将e赋值到新数组的e.hash & (newCap - 1)位置上
                        newTab[e.hash & (newCap - 1)] = e;
                     //e.next不为空且e的节点类型属于红黑树
                    else if (e instanceof TreeNode)
                          //调用红黑树的split进行复制
                          //这里入参的this是指当前对象,即当前的HashMap实例
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //e.next不为空且e的节点类型属于链表
                    //使用链表方法进行复制
                    else { // preserve order
                        
                      //构建2条不同的链表,XXHead和XXTail分别是头引用和尾引用
                          //其中一条lo链表上的值存储到的新数组j位置上,也就是和旧的数组上的位置一致,称为旧的索引位置上的链表
                         //另外一条hi链表上的值存储的新数组j+oldCap位置上,称为旧的索引+oldCap位置上链表
                      
                      
                        //lo开头的链表上面的数值存储位置和旧的数组上的位置一致
                        //loHead指向链表首位,loTail指向链表末尾
                        Node<K,V> loHead = null, loTail = null;
                          
                          //hi开头的链表上面的数值存储位置为旧的数组上的位置j加上oldCap
                          //hiHead指向链表首位, hiTail指向链表末尾
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        
                        //循环遍历链表,直到e的下一个节点为空                     
                        do {
                            next = e.next;
                            //判断e.hash & oldCap的值是否为0,若为0则存储位置和旧的数组上的位置一致
                            if ((e.hash & oldCap) == 0) {
                               //首次进来loTail为空
                                if (loTail == null)
                                   //将loHead指向lo链表首个节点
                                    loHead = e;
                                else
                                    //loTail的下一个节点指向满足条件的节点e
                                    loTail.next = e;
                                //将loTail指向lo链表的尾节点
                                loTail = e;
                            }
                            else {
                                //首次进来hiTail为空
                                if (hiTail == null)
                                     //将lhiHead指向hi链表首个节点
                                    hiHead = e;
                                else
                                   //hiTail的下一个节点指向满足条件的节点e
                                    hiTail.next = e;
                                //将loTail指向lo链表的尾节点
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        //判断lo链表是否为空
                        if (loTail != null) {
                            //将loTail的next引用赋值为null
                            loTail.next = null;
                            //将lo链表的头节点赋值给新数值的j位置
                            newTab[j] = loHead;
                        }
                          //判断hi链表是否为空
                        if (hiTail != null) {
                             //将hiTail的next引用赋值为null
                            hiTail.next = null;
                             //将lhi链表的头节点赋值给新数值的j + oldCap位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回新的数组
        return newTab;
    }

7.2 split方法

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
               //split方法是树叶红黑树TreeNode中的方法,因此这里的this是红黑树节点的实例
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            
             //构建2条不同的链表,XXHead和XXTail分别是头引用和尾引用
              //其中一条lo链表上的值存储到的新数组j位置上,也就是和旧的数组上的位置一致,称为旧的索引位置上的链表
              另外一条hi链表上的值存储的新数组j+oldCap位置上,称为旧的索引+oldCap位置上链表
                      
            //lo开头的链表上面的数值存储位置和旧的数组上的位置一致
            //loHead指向链表首位,loTail指向链表末尾
            TreeNode<K,V> loHead = null, loTail = null;
            
            //hi开头的链表上面的数值存储位置为旧的数组上的位置j加上oldCap
           //hiHead指向链表首位, hiTail指向链表末尾
            TreeNode<K,V> hiHead = null, hiTail = null;
            
            //lc 和hc分别统计两种不同链表中链节点的个数
            //lc为旧的索引位置上链表节点的个数
            //hc为旧的索引+oldCap位置上链表节点的个数
            int lc = 0, hc = 0;
            //循环遍历直到e的值为空
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                
                //判断e.hash & oldCap的值是否为0,若为0则存储位置和旧的数组上的位置一致
                if ((e.hash & bit) == 0) {
                  
                   //首次进来时loTail为空
                    //将loTail的值赋予e的前驱节点
                    if ((e.prev = loTail) == null)
                        //将e赋值给loHead 
                        loHead = e;
                    else
                        //将loTail的next引用指向e
                        loTail.next = e;
                     //将e赋值给loTail
                    loTail = e;
                    //旧的索引位置上链表节点的个数加1
                    ++lc;
                }
                else {
                    //首次进来时hiTail为空
                    //将hiTail的值赋予e的前驱节点
                    if ((e.prev = hiTail) == null)
                        //将e赋值给hiHead 
                        hiHead = e;
                    else
                        //将hiTail.的next引用指向e
                        hiTail.next = e;
                    //将e赋值给hiTail
                    hiTail = e;
                     //旧的索引+oldCap位置上链表节点的个数加1
                    ++hc;
                }
            }
            
            //旧的索引位置上的链表不为空
            if (loHead != null) {
               //如果旧的索引位置上的链表节点数小于等于6
                if (lc <= UNTREEIFY_THRESHOLD)
                    //调用untreeify方法将红黑树转换为链表 ,并将链表头节点赋值给tab[index]             
                    tab[index] = loHead.untreeify(map);
                else {
                    //将旧的索引位置上的链表头节点赋值给 tab[index] 
                    tab[index] = loHead;
                    //如果旧的索引+oldCap位置上链表的头节点不为空,说明
                    红黑树的节点分别位于两条链表上
                    if (hiHead != null) // (else is already treeified)
                       //将旧的索引位置上的链表重新树化
                        loHead.treeify(tab);
                }
            }
            
           // 旧的索引+oldCap位置上链表的头节点不为空
            if (hiHead != null) {
                //如果旧的索引+oldCap位置上的链表节点数小于等于6
                if (hc <= UNTREEIFY_THRESHOLD)
                 //调用untreeify方法将红黑树转换为链表 ,并将链表头节点赋值给tab[index+ bit]          
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    //将旧的索引+oldCap位置上的链表头节点赋值给 tab[index+ bit] 
                    tab[index + bit] = hiHead;
                    //如果旧的索引位置上链表的头节点不为空,说明
                    红黑树的节点分别位于两条链表上
                    if (loHead != null)
                         //将旧的索引+oldCap位置上的链表重新树化
                        hiHead.treeify(tab);
                }
            }
        }

7.3 untreeify

调用untreeify将红黑树转换为链表

final Node<K,V> untreeify(HashMap<K,V> map) {
            //hd为链表头引用,tl为链表尾节点引用
            Node<K,V> hd = null, tl = null;
            //循环遍历直到q的值为空
            for (Node<K,V> q = this; q != null; q = q.next) {
                //获取链表类型的节点p
                Node<K,V> p = map.replacementNode(q, null);
                //首次进来,tl为空
                if (tl == null)
                    //将p赋值给hd
                    hd = p;
                else
                    //非首次进来,将tl的next引用指向p
                    tl.next = p;
                //将p赋值给tl
                tl = p;
            }
            //返回头引用
            return hd;
        }

7.4 replacementNode

通过红黑树节点的哈希值和key,生成一个新的链表节点

 Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        return new Node<>(p.hash, p.key, p.value, next);
    }

8 参考文献

1)JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
2) JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/
3) Bruce Eckel. Java编程思想,第4版,2007,机械工业出版社
4)方腾飞,魏鹏,程晓明. Java并发编程的艺术,第1版,2015年,机械工业出版社