HashMap面试题

1.为什么使用红黑树?

在java8之前HashMap值由数组和链表两种结构组成这就使HashMap导致服务器Doc,加入红黑树是为了防止Doc攻击。
那什么是Doc攻击呢?
百度百科的定义:
在这里插入图片描述
更为贴切的解释:
在这里插入图片描述
总之,就是通过长时间处理同一个请求达到不断消耗cpu资源,使服务器瘫痪的目的。
那么,问题又来了,为什么Doc攻击会跟红黑树联系起来呢?
这就不得不谈论到tomcat和CVE-2011-4858(Apache Tomcat 资源管理错误漏洞)
Tomcat邮件组讨论
在这里插入图片描述
比如说有一个网站:
www.baidu.com? name=Lucy age=20 sex=1,…这样的参数个数可能很长很长甚至达到3000个以上 ,由上面的图可以知道,像 name=Lucy这样的一个都是以HashMap的键值对存储的,由此就会产生3000个以上的键值对,还有就是,如果不同的键产生了相同的hash值,那么它计算出来的index必然相同,HadhMap的源码就指明了相同的index的键值对会进行前插形成一个单链表。众所周知,单链表越长,遍历的效率越低,如果黑客利用这一点,发送一条参数很多的请求,然后构造HashMap,最后再带参访问,带参访问必然会涉及到遍历链表,如果说链表长度>2万同时查找的是末尾的数据,这就使得cpu不断轮询挨个儿查找,消耗了cpu资源,这就是doc攻击。而红黑树的加入不仅限制了键值对(在Apache Tomcat使用哈希表
用于存储HTTP请求参数)个数,而且红黑树的遍历效率相对链表来说大大提高。
黑客使用doc攻击还利用了String重写hashCode方法的弊端,举个栗子:“Aa”,“BB”,"C#"的hash值相同,都是2112,并且重写hashCode的方法明确指明了hash值的计算方法,黑客据此可以构造出一个同hash值不同key极其长的链表。

hash&(n-1) 一次运算
hash%n 多次运算

2.Hash算法改进

HashMap初始数组长度为16,即2的4次幂,违背了算法导论中除法散列法建议的数组长度不为2的n次幂,原因是:HashMap对Hash算法做了改进,让hash值不再仅仅依赖低四位,同时也依赖高位,这样就使得散列表分布更均匀,或者说使数组分布更均匀,同时做了这种改进的必要条件也是数组长度为2的n次幂,也就是说数组长度为2的n次幂才能获得散列表分布更均匀这种最终效果,这与算法导论中除法散列法期望达到的效果相差无几。

除法散列法在用来设计散列函数的除法散列法中,通过取k除以m的余数,来将关键字k映射到m个槽的某一个中去。亦即,散列函数为:
h(k)=kmod m例如,如果散列表的大小为m=12,所给关键字为k=100,则h(k)=4。这种方法只要一次除法操作,所以比较快。
当应用除法散列时,要注意m的选择。例如,m不应是2的幂,因为如果m=2,则h(k)就是k的p个最低位数字。除非我们事先知道,关键字的概率分布使得k的各种最低p位的排列形式的可能性相同,否则在设计散列函数时,最好考虑关键字的所有位的情况。练习11,3-3要求读者证明,当k是一个按基数2P解释的字符串时,选m=2P-1可能是个比较糟糕的选择,因为将k的各字符进行排列并不会改变其散列值。
可以选作m的值常常是与2的整数幕不太接近的质数。例如,假设我们要分配一张散列表,并用链接法解决碰撞,表中大约要存放n=2000个字符串,每个字符有8位。一次不成功的查找大约要检查3个元素,但我们并不在意,故分配散列表的大小为m=701。之所以选择701这个数,是因为它是个接近a=2000/3、但又不接近2的任何幂次的质数。把每个关键字k视为一个整数,则我们有散列函数:
h(k)=kmod 701

加载因子(扩容因子)为何是0.75?
1 空间利用率很高。提高查询成本(链表几率大)
解释:数组容量要全部用完,即每个位置都要有元素,但没有元素剩下的槽位越少,产生hash冲突的可能性越大,形成链表的可能性越大,链表的长度可能越来越长,查询效率越来越低,耗时长。

0.5 空间利用率低
解释:仅用一半数组长度就扩容,,查询效率高,但是空间利用率低。

因此0.75是在时间和空间上做一个折中

3.树化参数为8:

树化参数为8其实跟泊松分布有很大的关系,在统计扩容因子为0.75的情况下,链表长度为0的概率为0.60653066、链表长度为的概率为0.30326533…也就是说在链表长度为8概率是非常低的,这时候做树化的话性价比就很高,如果说在概率很多的时候动不动就做树化也就是说在链表长度为2或3的时候做树化,那树化的成本会很高(hash冲突导致链表长度为2或3概率非常大),导致HashMap的性能很差,因此我们在hash冲突概率很低的时候(当链表长度为8的时候,也就是说 0.00000006的概率出现hash冲突使链表长度为8)才会去做树化。

 * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606   // 0.01263606hash冲突的概率使链表长度为3
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

最后强调一点:链表长度为8不一定会在链表长度为8的数组位置树化,原因如下:

  /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;//反树化

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
     /**变成一个树时数组最小的容量条件,因此数组要紧过两次扩容并且链表长度为8才会在链表长度为8不一定会在链表长度为8的数组位置树化*/
    static final int MIN_TREEIFY_CAPACITY = 64;

因此当链表长度为8并且数组容量为16时仅会扩容。
在这里插入图片描述

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {         
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //需要判断(tab = table) == null的原因:HashMap构造方法并没有初始化数组,往往是在put方法里面初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //无冲突:计算数组下标,如果该下标对应的数组槽没有元素就将元素插进去,p表示当前数组槽中元素引用
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//有冲突
            Node<K,V> e; K k;
            //hash值相同,key不一定相同
            //hash值相同,key相同的情况下,替换原值,返回原值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          //hash值相同,key(槽中,但可能与链表中非槽中key相同)不相同的情况下,循环链表,尾部添加
            else {
                for (int binCount = 0; ; ++binCount) {
                //p和e都遍历,e比p快一步
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //TREEIFY_THRESHOLD为树化条件,值为8,循环了7次,链表中8个结点,触发树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //与链表中非槽中key相同,跳出到if (e != null) { // existing mapping for key,此时,e肯定不为空(为空就已经执行 p.next = newNode(hash, key, value, null)),一定能触发if (e != null),覆盖原值
                    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;
                //onlyIfAbsent写死了,为false,e.value = value;一定执行
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

有两处对返回值进行处理

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //树化条件,数组长度不满足MIN_TREEIFY_CAPACITY,仅扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
            //数组长度不满足MIN_TREEIFY_CAPACITY,树化
        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);
        }
    }
// resize()要么初始化,要么扩容
/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */

4.HashMap扩容机制:

扩容后要么插在原位置,要么插在原位置+原来数组长度
为什么会这样呢?这就要看源码中算法了
hash值101010110001001010101010101 1101

原来长度为16时,hash值有效位:
hash 1101
16-1 1111
1101&1111
扩容为32时
11101
11111
原位置的index值要加上多出来的最高位,由于此例中1&1=1,所以加10000(10进制的16),如果有hash值此为为0,则加0,即在原位置。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fire king

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值