jdk1.7的HashMap底层有数组加链表实现。
- 为什么HashMap的key对象重写equals方法需同时重写hashCode方法?
有人觉得get操作和put操作在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null。
如下面的例子:
package com.dalingjia.test;
import java.util.HashMap;
public class MyTest {
private static class Person {
int idCard;
String name;
public Person(int idCard, String name) {
this.idCard = idCard;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
//两个对象是否等值,通过idCard来确定
return this.idCard == person.idCard && this.name.equals(person.name);
}
}
public static void main(String[] args) {
HashMap<Person, String> map = new HashMap<Person, String>();
Person person = new Person(1234, "乔峰");
System.out.println(person.hashCode());
Person person1 = new Person(1234, "乔峰");
System.out.println(person1.hashCode());
//put到hashmap中去
map.put(person, "天龙八部");
//get取出,从逻辑上讲应该能输出“天龙八部”
System.out.println("结果:" + map.get(person));
System.out.println("结果:" + map.get(person1));
}
}
输出结果如下:
尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)–>hash–>indexFor–>最终索引位置 ,而通过key取出value的时候 key(hashcode2)–>hash–>indexFor–>最终索引位置,由于两个对象的hashcode()方法返回值不同,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)
代码如下:看if判断
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;
}
return null;
}
所以,某个对象在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)。
- HashMap的默认初始长度是多少?为什么这么规定?
HashMap的默认初始长度为16,并且每次自动扩展或是手动初始化时,长度必须是2的幂。之所以选择16,是为了服务于从key映射到index的Hash算法。取模运算效率较低,为了实现高效率的hash算法,HashMap的发明者采用了位运算的方式。源码如下:(length是HashMap的长度)
static int indexFor(int hash, int length) {
return hash & (length-1);
}
我们以值为“book"的key来演示:
- 计算book的hashcode,结果为十进制的3029737,转换为二进制为101110001110101110 1001。
- 假定HashMap的长度默认是16,计算length-1的15,二进制为1111.
- 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所有index=9。可以说hash算法最终得到的index结果,完全取决于key的hashcode值的最后几位。
假设HashMap的长度为10,重复上述步骤:
单独看这个结果,没什么问题,结果仍然是hashcode的最后几位,我们再试一个新的hashcode:
再试一个:
这样运算结果都是1001,导致有些index结果出现的几率变大,而有些index结果永远不会出现(比如0111),这样显然不符合Hash算法均匀分布的原则。反观长度为16或其他2的次幂,length-1的值是所有二进制位全为1,这样index的结果等同于hashcode后几位的值。只要输入的hashcode值本身是均匀的,那么hash算法的结果就是均匀的。
- 高并发情况下,为什么HashMap可能会出现死锁?
首先看一下1.7的扩容机制:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
很明显,当HashMap中元素个数大于等于阀值,且数组对应的下标元素不为null时,就会考虑扩容;但是如果此时的HashMap的长度为MAXIMUM_CAPACITY(1<<30)时,会将阀值设置为Integer.MAX_VALUE,不考虑进行扩容,直接插入元素。否则就进行扩容,并将旧数组中的元素rehash到新的数组中。代码如下:
//扩容操作
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
真正的rehash操作:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//1,遍历旧数组
for (Entry<K,V> e : table) {
//2,遍历旧数组某个元素对应的链表
while(null != e) {
//3,链表的下一个结点
Entry<K,V> next = e.next;
//4,重新hash
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//5,旧元素对应新数组的下标
int i = indexFor(e.hash, newCapacity);
//6,旧元素结点做头结点插入新数组
e.next = newTable[i];
newTable[i] = e;
//7,继续遍历链表的下一个结点
e = next;
}
}
}
现有A和B两个线程,对长度为4的HashMap做put操作,虽然size=3,大于等于阀值(4*0.75),但是数组对应的下标元素为null,不需要扩容,直接插入。这个问题在上面有说明。
此时达到resize的两个条件,两个线程各自进行resize的第一步操作:扩容
假如线程B遍历到Entry3对象,刚执行完红框中的代码,线程就被挂起,对于线程B来说:e = Entry3 , next = Entry2 ; 这时线程A畅通无阻地进行rehash操作,完成后,结果如下(图中e和next代表线程B的两个引用):
接下来线程B恢复,继续执行属于他自己的rehash操作,线程b刚才的状态是:e = Entry3 , next = Entry2 ;
当执行晚上面这一行代码,显然i = 3;应为线程A对于Entry3的hash结果也是3。
我们继续执行到这两行代码,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2,此时 e = Entry2 , next = Entry2 ;
接着又一轮循环,执行到下面红框中的代码:
e = Entry2 , next = Entry3 ;
接下来执行下面的三行代码,用头插法把Entry2插入到线程B的数组的头结点:
整体情况如下:
第三次循环开始,又执行到红框中的代码:
此时:e = Entry3 , next = Entry3.next = null ;
最后我们执行下面这行代码:
newTable[i] = Entry2 ;
e = Entry3 ;
Entry2.next = Entry3 ;
Entry3.next = Entry2 ;
即Entry3的引用指向Entry2, Entry2的引用指向Entry3,链表出现环形!如下图:
此时问题还没有直接产生,当调用get方法时,查找一个不存在的key,而这个key刚好定位到i = 3的位置,由于位置3带有环形链表,所以程序进入死循环!所以高并发下的HashMap1.7会出现并发问题。
总结:hashMap1.7不是线程安全的,在多线程并发的情况下可能会形成环形链表,让下一次读操作出现死循环。