HashMap是什么?
HashMap是一种使用了Hash算法的Java中的数据结构,内部主要由数组和链表实现。
备注链表是为了应付Hash碰撞时使用,如果没有碰撞,则不需要链表
HashMap的底层接口是Map
Map和Colletction没有关系
HashMap设计-均匀分布
因为HashMap设计是通过,Hash值的不同,放到不同的index下,所以,存在一些的设计,使得index的计算更加均匀。
HashMap当发生Hash碰撞时,从头插入还是从尾插入?
Java8之前是头插入,Java8之后是尾插入。
为什么修改?头插入时,HashMap在并发情况下的put操作在一定情况下会造成死循环。
HashMap在并发put操作时,为什么可能造成死循环?
首先,要明白,HashMap在put操作时的自动扩容内部顺序:当发生Hash碰撞时,先把元素放到计算所得索引的链表上,再新建数组进行扩容。而不是先新建数组,然后在把该元素放上去。
//put方法的最后
if (++size > threshold)
resize();
死循环和扩容代码相关
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//(关键代码)
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} // while
}
}
简单解释,头插、尾插
使⽤头插会改变链表的上的顺序,但是如果使⽤尾插,在扩容时会保持链表元素原本的顺序,就不会出
现链表成环的问题了。如果出现了环形列表,当扩容时候,就会在这个方法进入死循环。
HashMa的线程安全
1、扩容的时候,死循环
2、put操作时,当发生哈希碰撞,此时HashMap按照平时的做法是形成一个链表。如果有多个并发,可能导致出现,线程A认为索引处为空,进入判断准备进行插入。然后被挂起,线程B也认为索引处为空,把数据插入。之后,线程A进行运行,把线程B的数据覆盖。
HashMap存放元素时的索引是如何计算的?
1、计算Hash值,hash=key的hashCode 与(他自身)的异或
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // ^异或;>>> 无符号右移
}
?为什么要右移16位
2、计算索引
//Java7中的方法,Java8没有单独的方法但是原理相同。length表示HashMap的容量
static int indexFor(int h, int length) {
return h & (length-1); //X % 2^n = X & (2^n – 1) //用位运算(与)来代替取模运算,故length必须是2n的倍数
}
为什么在计算索引时,要用位运算代替取模运算?
因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高
HashMap的默认初始化长度是多少,为什么?
16=2^4,为什么不用2的其他次方?
倾向认为是一个经验值。
HashMap的容量必须是2的幂
就算你自定义初始容量不是2的幂,HashMap内部也会自动找到最接近的2的幂,并且设置。
为什么?
在计算Hash值时,用了用位运算(与)来代替取模运算,故length必须是2n的倍数
为什么HashMap要提前扩容?
猜测,可能为了Hash的均匀分布。
为什么重写equals⽅法的时候需要重写hashCode⽅法
1、首先,明确,equals相等,可以认为他们的hashCode必须相等。这是规定,因为Hash变换中,相同的、值通过Hash变换的值相等。
2、有些类,比方 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的
对象必须重写这两个方法。
3、如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。
HashMap什么时候会触发扩容操作?
HashMap中元素数超过容量*加载因子时,HashMap会进行扩容。
HashMap的负载因子为什么是0.75
加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。
在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作。
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择,
关于红黑树和HashMap
当链表长度超过阈值(8)时,将链表转换为红黑树