HashMap的数据结构
HashMap的底层实现原理是根据散列表来实现的,我们可以认为是"链表数组"来实现的;其数据结构如下图所示:
从上图我们可以看出哈希表是由数组+链表组成的,
数组是什么类型?
当我们在系统中创建hashMap时,系统中定义一个长度默认为16的(Node[])数组,源码如下所示:
transient Node<K,V>[] table;
数组中的元素存储的为链表的头结点Node,其类型如下图所示:
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash; //key对应的hash值
this.key = key; //key值
this.value = value;//value值
this.next = next; //下一个节点
}
存值put
要存值首先得确定要存储到数组的哪个下标位置,定位到了下标位置,因为数组中保存的是链表的第一个节点,还得判断保存在该位置的链表位置
如何确定元素存储在数组中的下标值?
如果给map中存一个元素,那么是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是该元素的key的哈希值对数组长度取模得到。比如key的哈希值为12则在数组中的下标位置为:12%16=12,28%16=12,108%16=12。所以12、28以及108都存储在数组下标为12的位置,这样表示他们在数组中存储的位置是相同的,也就是产生了碰撞,则数组下标为12的位置所存储的链表有三个节点。
如何确定链表中的存储位置呢?
已知数组中存储的是每个链表的头结点,链表的每个节点为一个静态内部类Node,其重要的属性有 key ,value, next,用来存储map中的键与值还有指向的下一个节点;代码如下所示:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //null总是放在数组的第一个链表中
int hash = hash(key.hashCode());//获取key值得hash值
int i = indexFor(hash, table.length);//根据hash值就可以定位到数组的下标位置
//遍历数组此位置的链表
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;
}
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); //参数e, 是Entry.next指向那个节点
//如果size超过threshold,则扩充table大小。再散列
if (size++ >= threshold)
resize(2 * table.length);
}
根据key的hash确定在数组中的下标位置;然后获取数组中该下标位置的所有链表节点并且循环遍历,判断每个节点的key值与当前key值是否相等,如果相等则替换掉旧值;如果不存在相等的key值,则将该key与value存储到新的Node中并且保存到链表的第一个位置,next指向之前旧的Node;也就是说数组中存储的是最后插入的元素.打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Node[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Node[0] = B
到这里为止,HashMap的大致实现,我们应该已经清楚
取值get
代码如下所示:
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
取值的时候,如果传的key值为null则直接调用查询空方法getForNullKey,此处不多追,如果key不为null则根据key值获取对应的hashcode从而定位到数组中的下标位置;然后循环遍历数组该位置的链表节点,如果key值相等,则返回对应的value,否则返回null
负载因子
当我们在代码中创建一个hashMap 如下图所示:
HashMap<Object,Object> hashMap=new HashMap<>()
默认调用构造方法 HashMap(int initialCapacity, float loadFactor)其中initialCapacity为默认数组大小为16,loadFactor为负载因子默认为0.75
负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费;源码中默认负载因子为0.75如图所示:
扩容
随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*负载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。