• java.util.HashMap源码分析


    在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。

    在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时,采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值,将链表存储转换成红黑树存储)

    HashMap的样子就变成下图的样子:

    下面对源码进行分析:

    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

    由HashMap的类声明中,可知继承自AbstractMap抽象类,实现Map、Cloneable、Serializable接口。

    public abstract class AbstractMap<K,V> implements Map<K,V>

    AbstractMap抽象类实现Map接口。

    观察常量:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    默认table初始容量16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    table最大容量2的30次方

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    默认负载因子0.75

    static final int TREEIFY_THRESHOLD = 8;

    结点冲突数达到8时,就会对hash表进行调整,如果table容量小于64,那么会进行table扩容,如果不小于64,那么会将冲突数达8那个单链表调整为红黑树

    static final int UNTREEIFY_THRESHOLD = 6;

    如果原先是红黑树的,resize后冲突结点数少于6了,就把红黑色恢复成单链表

    static final int MIN_TREEIFY_CAPACITY = 64;

    如果table的容量少于64,那么即使冲突结点数达到TREEIFY_THRESHOLD后不会把该单链表调整成红黑数,而是将table扩容

    Node静态类是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(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                        return true;
                }
                return false;
            }
        }

    四个构造方法:

    public HashMap(int initialCapacity, float loadFactor)

    指定初始容量和负载因子

    public HashMap(int initialCapacity)

    指定初始容量,使用默认负载因子0.75

    public HashMap()

    使用默认初始容量16,默认负载因子0.75

    public HashMap(Map<? extends K, ? extends V> m)

    将一对Key-Value加入HashMap中

    public V put(K key, V value) {
       //第四个参数是onlyIfAbsent,表示当找到同key的键值对,是否更新value的值,false是更   //新,第五个参数是evict,在afterNodeInsertion方法中使用,但这个函数是空函数
    return putVal(hash(key), key, value, false, true); }

    实际调用putVal方法

    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为空或者未分配空间,则resize,放入第一个K-V时总是先resize
    if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
        //(n-1)&hash计算K-V存的table位置,如果首节点为null,代表该位置还没放入过结点
    if ((p = tab[i = (n - 1) & hash]) == null)
            //调用newNode新建一个结点放入该位置 tab[i]
    = newNode(hash, key, value, null); else {//到这里,表示有冲突,开始处理冲突 Node<K,V> e; K k;
           //这时候p是指向table[i]的首个Node,判断table[i]是否与待插入节点有相同的
           //hash,key值,如果是则e=p
           
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
           //到这里说明table[i]的第一个Node与待插入Node的hash或key不同,那么要在
           //这个节点之后的链表节点或者树节点中查找
           else if (p instanceof TreeNode)//如果之后是树节点,使用树节点的插入方法
                             //插入成功后e等于null e
    = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//说明之后是链表节点 for (int binCount = 0; ; ++binCount) {//逐个向后查找                  
                
    if ((e = p.next) == null) {//如果下一个节点是null,表示没有找到                          
                                //同hash或同key的节点 p.next
    = newNode(hash, key, value, null);//新建一个节点
                                      //放在链表的最后
                 //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,             
                 //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行
    //resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
                  
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }//如果找到同hash或同key的节点,那么直接退出循环,
                 //此时e等于冲突Node
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//调整p节点,以继续查找 } }
           //退出循环后,先判断e是否为null,为null表示已经插入成功,不为null表示有冲突
    if (e != null) { // existing mapping for key V oldValue = e.value;//保存旧冲突点的value if (!onlyIfAbsent || oldValue == null)
             //判断是否需要修改value的值,默认是修改,如果旧冲突点value是null,一定          
              //是修改的 e.value
    = value; afterNodeAccess(e);//空函数 return oldValue; } } ++modCount;//当插入了新节点才会运行到这里,HashMap结构调整次数+1 if (++size > threshold)//如果HashMap插入新节点后大于threshold,进行扩容 resize(); afterNodeInsertion(evict);//空函数 return null; }
    resize方法
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;  //将原hash表赋值给oldTab临时变量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;//原hash表容量赋值给
                             //oldCap,如果第一次resize,oldCap为0
    int oldThr = threshold;//原扩容阀值赋给oldThr临时变量 int newCap, newThr = 0; if (oldCap > 0) {//如果不是第一次resize if (oldCap >= MAXIMUM_CAPACITY) {//如果原table容量已经达到最大值 threshold = Integer.MAX_VALUE; return oldTab;//无法继续扩容,只能返回原table } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)//扩容将原容量*2 newThr = oldThr << 1; // double threshold,新扩容阀值*2 }
        //原容量为0,但原阀值不为0,那么新容量为原阀值
    else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;
        //如果原容量和原阀值都为0,用默认值进行初始化
    else { // zero initial threshold signifies using defaults 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;//阀值被重新赋值 @SuppressWarnings({"rawtypes","unchecked"})
         //使用新容量创建一个新hash表 Node
    <K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;//旧hash表被新创建的表替换 if (oldTab != null) {//如果旧hash表不为null for (int j = 0; j < oldCap; ++j) {//遍历旧hash表,目的是重新分配所有
                              //结点到新hash表中 Node
    <K,V> e; if ((e = oldTab[j]) != null) {//如果oldTab[j]不为null,取出第一个                           //结点赋值给e oldTab[j] = null;//oldTab[j]赋值为null if (e.next == null)//如果只有一个结点,表示之前没有碰撞 newTab[e.hash & (newCap - 1)] = e;//把该结点存入新表 else if (e instanceof TreeNode)//如果e是树结点,表示要移动一棵                              //树
                  //调整这棵树 ((TreeNode
    <K,V>)e).split(this, newTab, j, oldCap); else { //到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重                //新计算在新表的位置,并进行搬运 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next;//记录下一个结点
                  //新表是旧表的两倍容量,实例上就把单链表拆分为两队,
                  //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
                    
    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) {//lo队不为null,放在新表原位置 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;//返回调整好的新表 }

    treeifBin方法是冲突结点达到8个,判断是要进行扩容还是链表转换成红黑色

    final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
        //table长度还未达到64,仅对table resize扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
        //否则要将单链表转换成红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do {//该循环将单链表的结点替换成TreeNode树结点,并构建双向链表,为构建红黑          //树作准备 TreeNode<K,V> p = replacementTreeNode(e, null); 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);//转换成红黑树 } }

    如果想了解Java 8 的HashMap对比Java 7的HashMap,性能提升,请看http://it.deepinmind.com/%E6%80%A7%E8%83%BD/2014/04/24/hashmap-performance-in-java-8.html

  • 相关阅读:
    第一次结对作业
    第二次编程作业
    第一次编程作业
    第一次博客作业*
    个人总结
    第三次个人作业
    第二次结对作业
    第一次结对作业
    第二次个人编程作业
    第一次个人编程作业
  • 原文地址:https://www.cnblogs.com/13jhzeng/p/5506554.html
Copyright © 2020-2023  润新知