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,即在原位置。