HashMap源码解析-java8

本文深入探讨Java中HashMap的实现机制,包括构造函数、底层思想、扩容机制、负载因子选择、哈希函数设计、链表与红黑树转换规则,以及与Java 7 HashMap的区别。同时,解析了HashMap线程安全问题、节点结构、红黑树排序规则等关键概念。

1.构造函数如下

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m)

2.底层实现思想

(1) 基于数组和链表实现,拉链法,数组存在扩容不存在缩容,链表在java8里加入了红黑树结构,见下文

(2) 在通过迭代器遍历HashMap的Node过程中,如果进行了结构性的更改,会fail-fast快失败,会抛出ConcurrentModificationException,api doc讲到依赖这个异常去纠错是不合理的,而是应该仅仅是去发现bugs

一、常见问题解答(大多是api doc上的原话或是看源码自己的总结)

1.扩容机制

翻倍扩容,容量变成原来的两倍,默认容量为16,也可初始指定容量,初始化的时候,阈值threshold为capacity,也就是说第一次threshold并不是等于capacity*loadFactor,当元素个数到达threshold时会扩容,即resize

2.负载因子为什么默认取0.75

负载因子高,空间利用率高,但是查询效率低,容易哈希碰撞。负载因子低,空间利用低,浪费空间。因此折中
另外,只有哈希碰撞严重时,才会出现红黑树结构,红黑树节点占用空间大约是普通节点2倍,实际上很少出现
理想情况随机哈希下,节点bucket上出现node个数和概率满足泊松分布,当负载因子是0.75时,泊松分布的lambda值是0.5,每个bucket链表节点node出现的个数和概率满足一个公式(见api doc)
当出现8-9个node时的概率,算法理想上只有千万分之1,即 1 in ten million

3.哈希函数是怎么设计的,哈希是怎么定址的

使用key.hashCode的高16位保持不变,低16位为高16位和低16位的异或,即(h = key.hashCode()) ^ (h >>> 16)
原因:通常的hashCode函数已经足够合理分布了,我们没有必要打乱他的节奏,考虑到位运算的便捷和快速,减少系统损耗,容量又是2的幂,哈希值只是比特位不一样,因此我们用高位和低位异或,加入对高位的影响
而哈希碰撞用红黑树处理。哈希定址采用哈希值对容量取模,源码中是通过与capacity-1进行位运算,因为位运算快

4.如果哈希碰撞严重,可能有哪些原因

可能是重写Key的哈希函数设计的不合理,尽量用Objects.hash即可,系统自带的。再就可能是负载因子设置的过高

5.链表和红黑树转换的规则是什么样的

每个桶上链表元素个数>=8个元素则转换为红黑树存储(并且满足capacity>=64),减少到<=6个又变回链表结构(扩容时,可能一条链变两条链,所以元素个数会减少),删除结点时,变回链表的触发条件因树的结构而异,此时树大概只有2-6个node,源码中的条件是root的左儿子的左儿子为空,具体源码注释有讲到

6.java7的HashMap和java8的HashMap有哪些区别

java8源码就增加了几千行,加入了很多默认函数,lambda等,更重要的是java7插入节点使用头插法(会产生环形链表死循环问题)和java8使用尾插法,哈希定址计算方式也不一样,并且java8引入了红黑树,这是java7不具备的。

7.java8的HashMap为什么也不是线程安全的

resize函数就是不安全的,还没复制完,另一个线程访问,此时table部分bin为null,源码中的处理是先开辟新数组,再复制元素。
put的时候也会出现问题,bin有值,然而读不到,本该形成链表,结果覆盖了另一个线程新put的值
size变量也不是volatile线程可见的,而且有++size操作

8.HashMap里结点Node的结构是什么样的

Map.Entry是Map接口里的public内部子接口
HashMap.Node是普通链表节点,是内部类,它实现了Map.Entry
HashMap.TreeNode是红黑树节点,是final内部类,它继承了LinkedHashMap.Entry,而后者又继承了HashMap.Node

9.Hashtable和HashMap的区别

一个区别是线程安全性,一个是key和value是否可以为null

10.HashMap.Node的hash属性和key属性为什么是final的

因为不能改变,hash不用重复计算,节约计算代价

11.红黑树的排序规则是怎么样的

红黑树的排序首先使用hash值比较,其次是Key的compareTo方法(判断实现Comparable接口),再是类名字符串等(如通过反射)和identityHashCode值排序,构造红黑树

二、源码解析

1.哈希计算方法

/**
 * 哈希计算规则
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2.容量capacity计算规则

/**
 * 打成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;
}

3.put方法底层原理

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //该bin是空,直接添加,注意哈希定址是位运算,i = (n - 1) & hash
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //key已存在,直接找到,就替换
            e = p;
        else if (p instanceof TreeNode)
            //如果是红黑树结构,通过红黑树方法进行插入新节点
            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);
                    //到达临界条件,链表变红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        //到达阈值,扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

4.扩容函数

/**
 * 扩容机制
 * @return the table
 */
final Node<K,V>[] resize() {
    //旧数组
    Node<K,V>[] oldTab = table;
    //旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //位运算capacity进行翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    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];//开辟新数组
    //记住,此时数组替换了,node元素还没过来
    table = newTab;
    if (oldTab != null) {
        //开始把旧数组的node元素复制到新数组中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            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;
                        //敲黑板,这里只判断最高位,如果不为0,那么hash值大于旧的容量,要放到高位的链表中,
                        //这就是扩容为什么一条链表可能变2条的原因
                        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;
                        //魅力之处,直接+oldCap定址,这是与java7的一个不同点
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回新开辟的数组
    return newTab;
}

5.把hashmap对应哈希值位置的bucket变成红黑树

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    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<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);
        //最后调用头结点的treeify方法,将其转变为具备父子关系的红黑树结构
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

三、红黑树结点内部的方法

1.把树的root放到链表的头

/**
 * 把root放到链表的头部去
 */
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        if (root != first) {
            Node<K,V> rn;
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

2.红黑树的二叉查找

/**
 * 从当前TreeNode往子孙节点搜索k
 */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        //红黑树是二叉有序的,二分搜索,log(n)复杂度
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        //首先比较哈希值,
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            //直接找到
            return p;
        else if (pl == null)
            //如果左边null,则到右边搜
            p = pr;
        else if (pr == null)
            //如果右边null,则到左边搜
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            //如果能通过compareTo判断,则这样继续搜索
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            //否则类似递归,继续find从右儿子搜索,直到找到
            return q;
        else
            //如果右儿子搜索没搜到,继续从左儿子往下搜
            p = pl;
    } while (p != null);
    return null;
}

3.建立红黑树,key怎么比较大小

//首先通过哈希值比较,然后通过key的compareTo方法,如果都无法比较大小,那么采用下面的方法比较大小
static int tieBreakOrder(Object a, Object b) {
    int d;
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        //如果通过类名字符串无法区分,用identityHashCode应该能区分吧,这是内存级别的
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}

4.把红黑树打成链表

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        //把树节点转换为普通链表节点,然后next串起来,prev之前有值不变
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    //返回头节点
    return hd;
}
//真的佩服源码的规范性,一般t代表temp临时变量,l代表左边,r代表右边,h代表head或high
//还有比如,hd代表head,tail代表尾巴,tl代表temp临时变量,等等

5.红黑树删除节点(不包含颜色调整),先看图解,如果不懂原理请后续关注我的红黑树基础-第二篇

这个函数主要是this是要删除的节点,找到节点s与之互换,然后用balanceDeletion调整,看下图中树结构的变化再看代码注释

/**
 * Removes the given node, that must be present before this call.
 * This is messier than typical red-black deletion code because we
 * cannot swap the contents of an interior node with a leaf
 * successor that is pinned by "next" pointers that are accessible
 * independently during traversal. So instead we swap the tree
 * linkages. If the current tree appears to have too few nodes,
 * the bin is converted back to a plain bin. (The test triggers
 * somewhere between 2 and 6 nodes, depending on tree structure).
 * @param map 该hashmap
 * @param tab hashmap内部Node数组
 * @param movable 是否需要移动root到链表头
 * @param this 待删除节点p
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
    int index = (n - 1) & hash;
    //root是根节点
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    //分别是this=p的链表指针的前驱和后继
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    if (pred == null)
        //如果要删除的p是链表的头,那么first = succ;并且tab[index] = succ;
        tab[index] = first = succ;
    else
        //否则链表断开this的链接
        pred.next = succ;
    if (succ != null)
        //链表断开this的链接,把链表关系完善
        succ.prev = pred;
    if (first == null)
        //空树
        return;
    if (root.parent != null)
        //获取到真正的根root
        root = root.root();
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        //根的左孩子的左孩子为空,基本上可以判断只剩2-6个node了,红黑树可以变成链表了
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    //主要关注这种左右孩子都非空的场景,为什么这种这么复杂,请看我写的关于红黑树基础的其他博客
    if (pl != null && pr != null) {
        //请看图解,我随便画了个图,节点名和变量名一致
        TreeNode<K,V> s = pr, sl;
        //先找到大于删除节点p的最小节点,为什么这么做,请看红黑树基础-第3篇
        while ((sl = s.left) != null) // find successor
            s = sl;
        //首先互换p节点和s节点的颜色,因为最终s要被换到p的位置,p要被换到s的位置
        //互换后,在p位置的s因为是p的颜色,所以不影响红黑树的性质
        //而换到s位置的p,颜色是原s节点的颜色
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        if (s == pr) { // p was s's direct parent
            //此时s==sp==pr,其实是建立p和s的关系,将else的内容简化了
            p.parent = s;
            s.right = p;
        }
        else {
            TreeNode<K,V> sp = s.parent;
            //建立p和sp的新父子关系,大家可以画图分析
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            //建立s和pr的新父子关系
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        //建立p和sr的新父子关系
        if ((p.right = sr) != null)
            sr.parent = p;
        //建立s和pl的新父子关系
        if ((s.left = pl) != null)
            pl.parent = s;
        //建立s和pp的新父子关系
        //如果pp为空,之前p就是根节点,那么现在s就是根节点了
        if ((s.parent = pp) == null)
            root = s;
        else if (p == pp.left)
            //如果原来p是pp的左孩子,互换后s就还是pp左孩子
            pp.left = s;
        else
            pp.right = s;
        //如果sr不为空,则互换后p有右孩子,没有左孩子,
        if (sr != null)
            //单链接情况直接用孩子替换
            replacement = sr;
        else
            //此时p没有孩子
            replacement = p;
    }
    //单链接情况,单单只有左孩子
    else if (pl != null)
        replacement = pl;
    //单链接情况,单单只有右孩子
    else if (pr != null)
        replacement = pr;
    else
        //p是叶子结点
        replacement = p;
    //互换后,如果p不是叶子结点,在树结构中直接把p节点干掉
    if (replacement != p) {
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        p.left = p.right = p.parent = null;
    }

    //回到红黑树的平衡删除了,如果要删除的节点是红色,那么直接删除即可
    //如果p是黑色的,那么此时就不满足红黑树的性质了,因为少了一个黑色节点,那么要进行balanceDeletion调整
    //注意:p的颜色是原来s的颜色
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

    //那么如果p是叶子结点,在树结构中直接把p节点干掉,detach断开连接
    if (replacement == p) {  // detach
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    //是否需要把root放到链表head
    if (movable)
        moveRootToFront(tab, r);
}

6.其他部分函数未完待续

红黑树源码部分(左右旋转,平衡插入和平衡删除请看我第三篇红黑树源码解析

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值