HashMap底层原理分析(结合面试问题分析)

1、 为什么HashMap底层数组的容量总是2的幂次方?
答:因为hashmap的底层在计算一个entry存放在数组中的索引值的时候,采用哈希值运算,如果经过哈希算法得到的一个哈希值h的后面的二进制表示为:0101 0101,此时的数组的长度length为2的4次方=16,在计算索引的最后一步就是int index = h & length-1;那么length的二进制表示为:0001 0000,length-1的二进制表示:0000 1111,后面全部为1,那么和h经过与操作之后实际上就是h的后面所表示的数字,范围0000-1111即0-15,所以就能保证我们计算出来的索引值不会越界。
在这里插入图片描述
2、 根据源代码可以知道,HashMap中的key是可以为null的。并且由于map的key是唯一性的,因此key为null的只有一个键值对,并且这个entry实体存放在数组中索引为0的位置。索引为0 的位置只有一个元素(这里主要和Hashtable做比较,因为该种结构是不允许键或者值为null)。
3、 为什么HashMap底层在生成哈希值的时候,在已经根据key生成哈希值的条件下,还需要进行右移运算和异或运算?
答:还是1中的例子,比如我们此时的数组长度为16,那么一个key经过哈希值运算之后,肯定是32位的,但是实际上,我们在计算索引的时候,只有哈希值的低位参与了计算,没有使用到高位的信息,导致很多不同的key经过运算得到的index都是相同的,从而造成数组中某个index的链表很长,检索变慢。当通过使用将高位右移运算,可使得高位的信息参与到index的计算过程中。使得索引更平衡。
4、 HashMap底层中,有个加载因子,其作用是什么?
答:这个加载因子决定了hashMap底层的table数组的扩容机制,例如:如果此时的table数组的长度为16,加载因子为0.75(底层源码默认),那么如果此时map中元素的个数(对应底层的一个size变量)大于了数组的长度乘以加载因子,此时数组会进行扩容。
5、 HashMap底层实现结构jdk7和jdk8的区别?
答:JDK1.7中,hashmap底层采用的是数组+链表的方式实现,里面的一个阈值就是加载因子,用于决定扩容的机制。而在jdk1.8中,底层采用的是数组+链表+红黑树的结构实现的,里面多了一个阈值叫做树化阈值 ,这个阈值默认值为8,也就是每个链表中的节点的个数大于等于8个的时候,就需要把这个链表树化成一个红黑树;除此之外,有一个树化阈值就有一个对应的反树化阈值(默认值为6) ,就是当我们将红黑树上的链表依次删除之后,如果这个树上的元素的个数小于等于6的时候,就会将红黑树链表提高添加效率但是查询效率也不会很低。
6、 分析一下,为什么jdk8中hashmap的树化阈值(8)和反树化阈值(6)不一样呢?
答:将两个阈值设置不一样是因为为了防止我们频繁在一个索引位置进行添加和删除元素,从而进行频繁的树化和反树化,例如:我们在index为6的位置添加一个元素之后,这个位置的元素个数变成9个,那么就需要进行树化,但是当我们在这个index删除一个之后,如果反树化阈值也是8,那么就需要进行反树化,这个过程是耗时的,因此将树化阈值设置的比反树化阈值大是有道理的。
7、 Jdk8中,hashmap底层为什么采用的是红黑树而不是其他的树形结构呢?
答:从分析hashmap的底层源码可以知道,hashmap在jdk7中采用的是数组+链表的形式,这样的方式对于键值对的添加很方便,但是对于数据的查询(获取)就需要依次从头结点开始往后遍历,查询效率低下,采用红黑树,可以平衡添加和查询两者之间的效率。
8、 注意:在jdk1.8中,只要一个index位置的元素的个数大于等于8就一定会进行树化吗?
答:不一定。在jdk8的源码中,进行树化之前需要进行一个阈值判断,就是判断当前这个table数组的长度是否小于MIN_TREEIFY_CAPACITY(这个表示树化的最小容量,默认值为64),只有当数组table的长度大于这个阈值的时候才会将数组中每个索引处元素个数大于8的链表转化成红黑树。因为如果数组的长度本身很短,例如16的时候,我们可以考虑数组扩容,这样的话可以增强map的散列性并且同样有机会将长度大于8的链表拆分短。

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);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

9、Jdk8中,table数组的初始化和数组的扩容都是采用的一个resize()方法进行实现的。在进行扩容的时候,我们考虑下面的情况,如果现在table的长度为4,并且在index1的位置有三个Node节点(jdk8中修改为节点,jdk7中叫做Entry),那么在进行数组的扩容的之后(扩容到8),index1位置处的元素应该在newTable的哪些索引处呢?
答:扩容之后节点存放的新索引和原来的索引是有一个规律的,要么就是原来的索引,要么就是原来的索引加上原来数组的长度。如果oldIndex =1,oldTabl.length = 4;那么newIndex=oldIndex=1||newIndex=oldIndex+oldTable.Length = 5。这个规律不是凭空来的。
我们举例来说明一下这个规律的由来:
假如:此时的table.length = 4;加载因子为0.75;map中元素的个数size=3,此时元素我们加入一个键值对(k,v)(考虑的是这个键值对对应的key不在map的key中),假设我们对k根据哈希值算法得到一个哈希值的二进制表示为:… 1001 0001(我们不考虑前面的那些);那么这个新的node应该存放的位置就是:index = hash & (table.length-1) = 1001 0001 & 0000 0011 = 0000 0001,存放在index==1的位置,当这个节点加入map之后,size++变成了4>table.lengthload_Factory = 3;所以需要进行扩容(jdk8中是先加入节点再进行扩容的),现在我们还是计算刚刚加入的那个节点的存放位置。
hash还是一样的1001 0001,此时的table.length = 2
4 = 8;新的index = hash & (table.length-1) = 1001 0001 & 0000 0111 = 0000 0001,还是原来的位置,但是如果刚刚k的hash=1001 0101呢?原来的index还是1,但是新的index = 1001 0101 & 0000 0111 = 0000 0101 = 5,变成了原来索引的值加上原来数组的长度。
10、 HashMap中resize()扩容方法原理分析


```java
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;
        }
        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];
    table = newTab;
//当原来的数组不为null,也就是这个resize的作用是扩容而不是初始化
    if (oldTab != null) {
//遍历底层的table数组,也就是每个桶的位置,每个桶中都是一个链表
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
//将桶中的头节点放入赋值给变量e,如果e不为null说明桶中有节点,否则就没有节点,进行下一个桶的判断
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;  //这一步其实没有多大必要,但是可以帮助jvm进行垃圾回收
//如果桶中只有一个节点,也就是链表只有一个节点,将这个节点的key的hash值与新的容量的长度-1进行与操作得到这个节点在新数组中的index,并且将新数组中index位置的值设置为这个节点
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
//如果这个节点是树节点,说明当前这个桶中的数据已经不再是链表了,而是一个红黑树,这里就进行数的分割再分配到新数组的各个桶中
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//否则就说明这个桶的链表中节点个数不为1,并且不是一个红黑树
                else { // preserve order
//定义四个节点变量,loHead用于表示低索引桶中链表的头节点;loTail表示低索引桶中俩表的尾部节点;hiHead表示高索引桶中链表的头结点;hiTail表示高索引桶中链表的尾部节点。(这里之所以有高低索引的区分是因为:在oldTable中索引为index桶的链表中的所有节点,在进行扩容之后,这些元素要么放在新数组对应的index桶中,要么就放在新数组index+oldCap对应的桶中,这个规律是因为我们采用2的幂次方作为数组的长度,并且采用index = hash & table.length-1作为index的计算公式)
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
//这里的if,else主要是判断当前这个节点放在新数组的低索引桶中还是高索引桶中
                        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;
}

**11、	HashMap底层为什么采用红黑树不采用其他树?**
答:因为红黑树可以实现键值对的查找和插入两种操作之间的性能平衡,如果采用严格的avl(平衡二叉树)实现,每次进行键值对插入或者删除的时候,为了保持这棵树的平衡性,需要对树结构进行旋转的可能性大,导致插入或者删除键值对的效率较低,但是avl树的查询效率高,而红黑树是和平衡树类似的结构,但是其平衡的要求没有那么严格,因此为了平衡插入和查找的效率,采用红黑树实现。
**12、红黑树的基本特性有哪些?**
(1)树上的每一个节点要么是黑色要么是红色;
(2)红黑树的根节点一定为黑色;
(3)如果一个节点为红色,那么该节点的父节点和其子节点一定不能为红色;
(4)任何一个节点到该子树对应的所有叶子结点的可能路径上,黑色节点的个数一样。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值