HashMap源码分析

☆樱花仙子☆ 提交于 2019-12-15 12:31:19

HashMap


hashmap的实现原理:首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中

当table中的元素足够多时,发生冲突的概率就会大大增加,冲突的增多会导致每个桶中的元素个数变多,这样的话会使得查找元素效率变得低下,当同一个桶中元素个数达到8时,桶中的元素结构将转换为红黑树。

基本属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    //序列号
    private static final long serialVersionUID = 362498820763181265L;
    //默认初始容量2^4=16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当桶(bucket)上的结点大于这个值就会转换成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    //当桶(bucket)上的结点小于这个值就会转换成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    //桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组,总是2的幂次倍
    transient Node<K,V>[] table;
    //存放具体元素的集
    transient Set<Map.Entry<K,V>> entrySet;
    //存放元素的有效个数
    transient int size;
    //记录修改次数
    transient int modCount;
    //临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    //填充因子
    final float loadFactor;

构造函数

无参构造

public HashMap() {
    //初始化填充因子
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

HashMap(int,float)型构造函数

需要对传入的初始容量和填充因子进行校验,初始容量转换为大于等于且最接近初始容量的2的幂次方整数

public HashMap(int initialCapacity, float loadFactor) {
    //初始容量不能小于0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                            initialCapacity);
    //初始容量不能大于最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //填充因子不能小于等于0,不能为非数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                            loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

//验证是否为非数字,非数字就会返回true,
public static boolean isNaN(float v) {
    return (v != v);
}
System.out.println(Float.isNaN(0.0f / 0.0f));  // true
System.out.println(Float.isNaN(0.0f));   // false

//返回一个大于等于且最接近 cap 的2的幂次方整数
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor()这个方法的作用是找到大于等于给定容量的最小2的次幂值
0000 0101------5
cap-1=4
0000 0100------4
右移1----0000 0010----或----
0000 0110
右移2----0000 0001----或----
0000 0111
右移4----0000 0000----或----
0000 0111-------7

n+1=8

  • 减一:如果给定的cap已经是2的次幂,但是不进行-1操作的话,那么得到的值就是大于给定值的最小2的次幂值

  • 右移16位就可以得到最大值32位1,再往后没有意义

HashMap(int)型构造函数

调用HashMap(int,float)型构造函数,填充因子为默认值

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

HashMap(Map)型构造函数

public HashMap(Map<? extends K, ? extends V> m) {
    //初始化填充因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //将m中的所有元素添加至HashMap中
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    //先判断m中是否有值
    if (s > 0) {
        //判断table是否已经初始化
        if (table == null) { // pre-size
            //数组为空,初始化参数
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                //临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
                threshold = tableSizeFor(t);
        }
        //数组不为空,超过阈值就扩容
        else if (s > threshold)
            resize();
        //将m中的所有元素添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //原容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //原临界值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //原table不为空,不是第一次扩容
    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
    }

    /**
    * 从构造方法我们可以知道
    * 如果没有指定初始化容量initialCapacity, 则不会给threshold赋值, 该值被初始化为0
    * 如果指定了initialCapacity, 该值被初始化成大于initialCapacity的最小的2的次幂
    */

    //这里这种情况指的是原table为空,并且在初始化的时候指定了容量,threshold被赋有值
    else if (oldThr > 0)
        // 则用threshold作为table的新容量,实际大小
        newCap = oldThr;
    //这里情况是原table为空,构造方法中没有制定容量,threshold=0
    else {
        //使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    //计算指定了initialCapacity情况下的新的threshold临界值
    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;
    //如果原来的table有值,将数据复制到新的table中
    //分红黑树和链表讨论
    if (oldTab != null) {
        //根据容量循环,将非空元素赋值
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //获得数组的第j个元素
            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;
}

HashMap常用方法

put方法

JDK8 HashMap put的基本思路:

1)对key的hashCode()进行hash后计算数组下标index;

2)如果当前数组table为null,进行resize()初始化;

3)如果没碰撞直接放到对应下标的位置上;

4)如果碰撞了,且节点已经存在,就替换掉 value;

5)如果碰撞后发现为树结构,挂载到树上。

6)如果碰撞后为链表,添加到链表尾,并判断链表如果过长(大于等于TREEIFY_THRESHOLD,默认8),就把链表转换成树结构;

7)数据 put 后,如果数据量超过threshold,就要resize。

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

hash方法

从代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)

//hash处理
static final int hash(Object key) {
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16)  为第二步 高位参与运算
//高16位与低16位进行异或为第三步,异或0不变
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xoroshc-1576380702804)(images/hash.jpg)]

map中put、get操作时为什么不直接用原有的hash值?

这个与HashMap中table下标的计算有关。

因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。
假设table.length=2^4=16

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P6V9Wt1f-1576380702807)(images/index坐标.jpg)]

这样做很容易产生碰撞。设计者权衡了speed, utility, and quality,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

putVal方法

@param hash --key的哈希值
@param key --key
@param value --key将要映射的value
@param onlyIfAbsent --如果是true的话,将不会改变已存在的值
@param evict --这个参数如果为true,那么每插入一个新值,就会把链表的第一个元素顶出去,保持链表元素个数不变

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未初始化,则先重新调整大小至初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(n-1)& hash 这个地方即根据hash求序号,存放位置
    if ((p = tab[i = (n - 1) & hash]) == null)
         //不存在,没有碰撞冲突,则新建节点
        tab[i] = newNode(hash, key, value, null);
    else {
    //节点存在值,发生冲突,存在节点p

        Node<K,V> e; K k;
        //先找到对应的node
        // 若桶中第一个结点的hash值相同并且equals方法返回true时进行替换
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

        else if (p instanceof TreeNode)
            //判断是否为红黑树,如果是树节点,则调用相应的putVal方法
            //todo putTreeVal
            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);
                    //判断链表长度是否大于8
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        //如果链表长度达到树化的最大长度,则进行树化
                        //todo treeifyBin
                        treeifyBin(tab, hash);
                    break;
                }
                //匹配到p的下一个节点,key存在跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;

                p = e;
            }
        }

        //如果已存在该key的映射,则将值进行替换
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //todo
            afterNodeAccess(e);
            return oldValue;
        }
    }

    //修改次数加一
    ++modCount;
    if (++size > threshold)
        //todo
        resize();
    //todo
    afterNodeInsertion(evict);
    return null;
}

寻找key对应的映射关系:

e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
HashMap中通过hash & (length - 1)计算得到的索引位置,因此,不同的hash值可能会映射到同一个桶中,所以在寻找key对应的映射关系时,首先通过这个e.hash == hash条件过滤掉hash值不同的key,之后再通过((k = e.key) == key || (key != null && key.equals(k))),提高效率

treeifyBin方法

当桶中链表长度超过8时,会调用此方法将链表结点转红黑树结点进行如下操作:

1)判断是否真的需要转换红黑树,如果数组长度小于MIN_TREEIFY_CAPACITY将会中扩容代替转换红黑树

2)如果符合转换的条件,先为链表转化红黑书做准备。将所有的节点转换成树形节点,并且构造成双链表。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    //n是数组长度,e是hash值和数组长度计算后,得到链表的首节点
    int n, index; Node<K,V> e;

    /** 如果数组为空或者数组长度小于树结构化的最小限制
    * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
    * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后得到的坐标结果相同。(并不是因为这些key的hash值相同)
    * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
    */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();


    /**Node对象转换成了TreeNode对象
    *把单向链表转换成了双向链表
    */
    // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
    // 根据hash值和数组长度进行取模运算后,得到链表的首节点
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;//hd是树首节点,tl是树尾节点
        do {
            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); // 继续遍历链表

        // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表


        // 把转换后的双向链表,替换原来位置上的单向链表
        if ((tab[index] = hd) != null)// 若指定的位置头结点不为空则进行树形化
            hd.treeify(tab);// 根据链表创建红黑树结构
    }
}

// For treeifyBin
//节点转换成树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

treeify方法

链表转换成红黑树结构

红黑树转换步骤:

1)遍历链表中节点,桶中第一个结点作为红黑树根节点(黑色)

2)内层遍历树结点,从根结点开始通过比较哈希值寻找其位置

3)若x结点hash值小于p结点hash值,往p结点左边寻找,否则往p结点右边寻找

4)一直按照步骤③寻找,直至寻找的位置为null即此位置为x的目标位置

5)因为红黑树性质,其插入删除都需要平衡调整

6)最后确保红黑树根结点为桶中第一个节点

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;//定义树的根节点

    /**
    *外层对链表遍历,x指向当前节点
    */
    for (TreeNode<K,V> x = this, next; x != null; x = next) {// 遍历链表,x指向当前节点、next指向下一个节点
        next = (TreeNode<K,V>)x.next;//下一个节点
        x.left = x.right = null;

        if (root == null) {//还没有根节点,根节点指向当前节点
            x.parent = null;
            x.red = false;
            root = x;
        }

        else {//根节点已经存在
            K k = x.key;//取当前链表节点的key
            int h = x.hash;//取当前链表节点的hash
            Class<?> kc = null;// 定义key所属的Class


            /**
            *对树遍历,将当前结点x的hash与树上的树节点hash进行比较
            */

            for (TreeNode<K,V> p = root;;) {// 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出,p是当前树节点
                int dir, ph;// dir 标识方向(左右)、ph标识当前树节点的hash值
                K pk = p.key;// 当前树节点的key
                if ((ph = p.hash) > h)
                    dir = -1;// 标识当前链表节点会放到当前树节点的左侧
                else if (ph < h)
                    dir = 1;//右侧

                /*
                 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
                 * 如果还是相等,最后再通过tieBreakOrder比较一次
                 */
                else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;//保存当前红黑树的树节点

                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
                 * 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点  再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
                 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
                 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
                 */
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;//当前树节点 作为 当前链表节点的父节点
                    if (dir <= 0)
                        xp.left = x;//作为左孩子
                    else
                        xp.right = x;//右孩子
                    root = balanceInsertion(root, x);//重新平衡
                    break;
                }
            }
        }
    }

    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 为了保证每次红黑树的根节点都在链表的第一个位置,在操作完成之后 需要moveRootToFront方法来进行调整

    moveRootToFront(tab, root);
}

balanceInsertion:指的是红黑树的插入平衡算法,当树结构中新插入了一个节点后,要对树进行重新的结构化,以保证该树始终维持红黑树的特性。

关于红黑树的特性:

性质1. 节点是红色或黑色。

性质2. 根节点是黑色。

性质3 每个叶节点(NIL节点,空节点)是黑色的。

性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质5. 从任一节点到其每个叶子的路径上包含的黑色节点数量都相同。

moveRootToFront:当我们删除或者增加红黑树节点的时候,root节点在双链表中的位置可能会变动,为了保证每次红黑树的根节点都在链表的第一个位置,在操作完成之后 需要moveRootToFront方法来进行调整。

get方法

1)先匹配第一个节点,如果匹配到直接返回

2)如果有冲突,判断如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key;如果是链表,则采用遍历查找

/**
    * 返回指定键映射的值,当该key不存在的时候返回null
    */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
    * 实现了Map.get 和相关方法
    */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        //判断是否是第一个节点
        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)
                //如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key
                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;
}

resize方法

(k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key
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;
}


### resize方法

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!