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

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年,机械工业出版社

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值