Java集合复习之HashMap-JDK10

1、概述

HashMap的底层实现是数组+链表+红黑树,JDK1.8之前的HashMap使用的是数组加链表,哈希函数取得再好也无法保证均匀分布,当哈希桶中有大量的数据的时候,HashMap就相当于一个单链表,时间复杂度为O(n,就失去了HashMap应有的优势,因此引入了红黑树,当哈希桶中的元素数量大于TREEIFY_THRESHOLD值时就转换为红黑树。

2、几个属性

// 创建 HashMap 时未指定初始容量情况下的默认容量,默认16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// hashmap的最大容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

// hashmap默认的负载因子是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 下面三个是HashMap中关于红黑树的三个参数
// 用来确定何时将链表转换为树
static final int TREEIFY_THRESHOLD = 8;

//用来确定何时将树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;

// 当链表转换为树时,需要判断下数组的容量,当数组的容量大于这个值时,才树形化该链表;
// 否则会认为链表太长(即冲突太多)是由于数组的容量太小导致的,则不将链表转换为树,而是对数组进行扩容;
static final int MIN_TREEIFY_CAPACITY = 64;

3、关键函数

①、treeifyBin(Node[] tab, int 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();
        //如果满足了树形化的条件,则进行树形化,e为指定位置桶里的链表节点,从第一个开始
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;//红黑树的头尾节点
            do {
            //新建一个树形节点内容和e一样
                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);
        }
    }

小结:上述方法做的事情如下:

  • 检查是否满足树形化的条件
    • 不满足则扩容数组
    • 如果满足
      • 遍历桶中的元素,创建相同数量的树形节点,复制内容,创建联系
      • 让桶第一个元素指向树头节点,替换桶中的链表为树形结构

上述函数前面部分实现的只是一个二叉树,而没有实现红黑树的操作,但在最后调用了hd.treeify(tab)实现了构造红黑树,源码如下:

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; 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 { //x指向树中的某个节点
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {//从根节点开始,遍历所有节点和当前节点比较然后调整位置
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);//比较哈希值

                        TreeNode<K,V> xp = p;
                        //把当前节点作为x的父亲,若x的哈希值比当前节点小则,x就是左孩子,否则为右孩子
                        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(tab, root);
        }

由上可知,在二叉树转换为红黑树时要保证有序,上述函数有个双重循环,用树中所有节点与当前节点进行比较哈希值(如果哈希值相等,就对比键,这里不用完全有序),然后根据比较结果确定在树中的位置。

②、putTreeVal()
如果在添加元素时,相应位置已经是红黑树了,则需要调用红黑树添加元素的函数putTreeVal(),源码如下:

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root() : this;
            //每次添加元素从根节点遍历对比哈希值
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;//如果哈希值和键值都相等就返回当前节点
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                         //如果当前节点和要添加的节点哈希值相等但键值不是同一个类,则挨个对比左右孩子
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;//如果从ch所在的自述中可以找到要添加的节点,则直接返回
                    }
                    //哈希值相等,但键无法比较只好通过特殊方法给个结果
                    dir = tieBreakOrder(k, pk);
                }
 				//上述操作得到了当前节点和要插入节点的大小关系
                TreeNode<K,V> xp = p;
                //判断当前节点有无左右孩子,没有左右孩子时才能插入,否则进入下一轮循环
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                        //插入后的平衡红黑二叉树的操作
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }
        
	//这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
    //这里源码注释也说了,这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
     static int tieBreakOrder(Object a, Object b) {
        int d;
        if (a == null || b == null ||
            (d = a.getClass().getName().
             compareTo(b.getClass().getName())) == 0)
            d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                 -1 : 1);
        return d;
    }

由上面方法可知,当红黑树添加元素时的流程如下:

  • 从根节点遍历,对比当前节点和待插入节点的哈希值;
  • 如果哈希值相等,键也相等,就返回当前节点,判断已存在这个元素;
  • 如果哈希值就通过其他信息,比如引用地址来给个大概比较结果,这里可以看到红黑树的比较并不是很准确,注释里也说了,只是保证个相对平衡即可;
  • 得到哈希值比较结果后,如果当前节点没有左右孩子时可以插入,否则进入下一轮循环;
  • 插入后进行平衡调整

参考文章:Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构

有关红黑树的知识推荐阅读:
重温数据结构:深入理解红黑树
Java数据结构和算法(十一)——红黑树

其余方法源码见:散列表及HashMap简析

你可能感兴趣的:(面试题)