您的位置:首页 > 其它

HashMap的工作原理

2016-04-23 22:27 302 查看
在面试的时候,会经常问到HashMap的一些情况。如果不了解HashMap的源码,对于该问题,实在无法完美的回答。大致问题如下:

1.HashMap和Hashtable的区别:

   HashMap允许null值的键和值,而Hashtable不能;HashMap非前程安全,而Hashtable是线程安全的。所以HashMap的效率要比Hashtable高。

2.HashMap的工作原理:

    底层使用哈希表来存储数据,也即数组+链表的形式。每一个bucket的位置都存放的是一个Entry对象。Entry对象有三个重要的属性:key,value,next。我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们使用put方法来传递key和value的时候,会先调用hashCode()方法,返回的hashCode用于找到bucket来存放Entry对象。HashMap是在bucket中存储键对象和值对象,作为Map.Entry。

3.put的原理,首先看一下源码,如下:

 

public V put(K key, V value) {
if (key == null)
return putForNullKey(value);  //null值总是放在数组的第一个链表中
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果key在链表中存在,就替换为新的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)  // 如果超过threshold,就进程扩容。再散列
resize(2 * table.length);
}


     在Entry对象里面,有next属性,作用是指向下一个Entry。比如,刚开始的时候,是A,通过计算其key的hashCode,放在了index=0这个位置,记作Entry[0]=A。后来又来了一个B,计算其key值也应该在index=0这个位置。此时HashMap会使B.next=A,Entry[0]=B。再后来,来了一个C,index=0,那么C.next=B,Entry[0]=C。可以发现,在index=0这个位置,存放了A,B,C这三个键值对,他们通过next属性来进行链接在一起。也就是数组中存放的是最后插入的元素。

4.当两个对象的hashcode相同会发生什么?

    因为hashcode相同,所以会发生“碰撞“,又因为HashMap使用LinkedList存储对象,这个Entry会存储在LinkedList中。

5.如果hashcode相同,它们是如何获取到对象的?

  首先看一下get的源码:

public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}

通过源码可以看到,当我们调用get()方法的时候,HashMap会使用键对象的hashcode找到bucket位置,然后调用key.equals()方法去找到LinkedList中正确的节点。在此之前,hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。

6.如果HashMap的大小超过了负载因子定义的容量,会怎么办?

    HashMap默认的负载因子是0.75,也就是说,当一个map填充了75%的bucket的时候,将会创建原来HashMap大小两倍的bucket数组,来重新调整map的大小,并将原来的对象放入到新的bucket数组中。这个过程叫做再散列。

7.重新调整HashMap大小会出现什么问题?

    当重新调整HashMap的时候,存在条件竞争,如果两个线程都发现HashMap需要重新调整大小,它们会同时试着调整大小。在调整的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部。如果资源竞争了,就会出现死锁。不过,在多线程中一般不用HashMap。

8.最后总结

     HashMap采用hashing的原理,通过put()和get()来存储和获取对象。当我们将键值对传递给put()方法时,它调用对象的hashCode()方法计算出hashcode,然后找到对应的bucket位置来存放对象。当获取对象时,通过键对象的equals()方法找到正确的键值,然后返回值对象。HashMap使用LinkedList来解决碰撞冲突,当发生碰撞,对象将会存储在LinkedList的下一个节点当中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: