说起HashMap,还是在2018年有换工作的念头的时候,有师兄内推了oppo的面试,那时候啥也没准备,两眼一抹黑就去面试了,然后面试官问我的第一个问题就是HashMap是怎么实现的,底层数据结构是什么,毫不奇怪的就挂了。只能说幸好当时是电话面试,要是现场面试的话,那真的是无地自容。现在,就让我从HashMap开始,重新准备面试吧。
HashMap的底层数据结构
HashMap是由数组和链表组成的。数组里放的对象,在java7是Entry,在java8是Node,都是key-value键值对的形式。
HashMap的put方法
put方法源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先,会通过hash方法计算key的hash值。在java8中,首先计算出key的hashcode,然后再用这个hashcode和它自己的高16位做异或,最终得到的值就是key的hash值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后再调用putVal方法。HashMap的底层数据结构是数组,数组的下标就是数组长度减一与key的hash值做与运算。如果该下标没有值,就将这个值存进去,如果发生冲突,该下标有值,就将这个值插入链表尾部,如果链表的长度超过8,就把这个链表转化为红黑树。插入值之后,如果存放的值的个数大于阈值(默认为数组长度*0.75),就会进行扩容。HashMap的扩容
当HashMap中存放的值超过设置的阈值时,为了减少hash冲突,就会发生扩容。HashMap的默认大小是16,如果认为设置了HashMap的大小,在初始化的时候,会变成和这个之最接近的,比它大的2的倍数。这样做的目的是为了在计算数组下标的时候,减少冲突。
HashMap在扩容的时候,若原来的大小为n,首先生成一个长度2n的数组,然后将原数组的值挪到新数组中。如果原数组中,没有hash冲突,则重新计算hash值,插入新数组。如果在原数组中发生了hash冲突,以链表的形式存储,则把该节点e(下标记为j)的hash值与n做与运算。如果所得结果为0,则把e放到新数组同样下标的位置,如果不为0,则e在新数组的下表为j+n。
在java7的时候,插入链表采用头插的方式,在多线程的情况下,扩容时修改链表的引用关系,有可能出现死循环的情况。java8采用尾插,不会改变链表的结构,可以避免这种情况发生。但是由于put和get方法,没有加锁,所以会存在get到的值被其他线程修改,不是put进去的值,所以依旧不是线程安全的。
HashMap的get方法
首先通过key的hash值得到数组下标,然后遍历链表,通过equals方法,比较key的值。