大家都知道,HashMap是我们平时在编程过程中经常用到的一个容器类,我们大部分的coder都只是知道其比较快,特别是在查找的时候,但是HashMap内部具体是如何实现的,如何保证这种实现能够很快的去让我们查询到我们要的结果呢。
在说HashMap之前我们需要了解一个数据结构,那就是链表,链表我们分为单向链表和双向链表,这里我说一下单向链表,其主要的思路就是将许多个对象基于next指针来绑定成一串数据,类似于如下的结构
在对数据进行遍历的时候,不管的通过Next,能够得到下一个记录。这种数据结构能够让我们很方便的找到其下一个记录或者下面第几个记录(当然比不上数组快),这种数据结构最好的地方在于,能够很容易的在某两个记录之间插入一个记录,这种操作的效率和方便性是数组没有办法比拟的,只需要将前一个数据的Next指向新的待插入的记录,然后将新的待插入的记录的Next设定为原来的Next指向的那个记录即可,伪代码可以是
Obj B = A.next;
A.next = NewObj;
NexObj.next = B;
HashMap中也使用到了这种数据结构,另外HashMap还用到了数组,基于这两个数据结构,就组成了我们常常使用到的HashMap的基本。
现在就开始来介绍一下HashMap具体内部结构。
首先HashMap内部定义了一个Entry[]数组,这个Entry对象定义了一个key,一个value,一个next以及一个hash四个成员变量,其中next的类型就是Entry,所以Entry其实是实现了链表的功能。
接着我们来看HashMap提供的各个方法的具体内部实现。
首先看构造方法,构造方法一般支持传入初始化长度或者无参,在这两种情况下的处理方式有些许差异,如果传入了参数为a,那么最终这个HashMap会申明多长呢?在计算机中,2是一个非常特殊的数字,2的次幂也是非常重要,所以这里,在初始化长度的时候,会初始化成一个超过a的第一个2的n次幂的数字长度,比如传入的数字为7,那么最终这个HashMap的长度就是8(2的3次幂);假设这里不传入一个长度值,那么默认的长度就会是16。而这个长度其实最终体现在哪里呢,就体现在这个Entry数组的长度。
HashMap还有一个特殊的属性,就是threshold,这个值其实来自于Map.length*factor,这个factor使用者可以控制,算是一个因子,就是说让这个Map中的记录个数超过了这个Map.length*factor之后,这个Map就需要扩容了,不然的话,会影响到查询效果,越来越让Map成为一个链表被使用。
看完构造方法,我们再看下put方法
if (key == null)
return putForNullKey(value);
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;
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;
上面是put方法的全部,首先是判断key是否为空,如果为空,则将Map中key为空的记录中的value替换成新的value。
如果key不为空,则先拿到key的hashCode,这个具体不同的类似算法不一样,拿到hash码之后,基于hash码和这个Entry数组的长度做运算,得到这个key应该被放置到这个Entry数组的第几个位置上,然后遍历这个Entry对象,基于next遍历,如果找到了一个其key和这个put进来的key一样,则替换这个key对应的value。
如果最后没有找到key一致的,则addEntry(hash,key,value,i),这个方法其实最终就是将新加的记录插入到这个Entry的最前面,为什么不追加到最后面,估计是考虑到后被添加上的能够先被遍历到。
上面就是put方法的实现,接着看get方法,如果理解了put方法,那么get方法就比较好理解了
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;
先如果key为空,则直接找到key为空的记录即可(key为空,hash码为0,所以永远放置在Entry[0]的位置)
如果key不为空,则先拿到其hash码,然后通过indexFor得到其在Entry数组中的位置,然后拿到这个Entry对象,基于链表的next进行遍历,找到key一致的数据即可。
最后还有一个地方要关注的,就是当Map中的记录个数超过了Map.size*factor的时候,HashMap都会将Entry数组扩大一倍,这样就会造成一个情况,就是老的Entry数组上的数据全部都要拷贝到新的Entry数组中去,处理的逻辑就类似前面的put方法了。先通过hash得到其应该放置的Entry数组中的位置,然后添加到这个Entry的最开头。