注:本文基于JDK 1.8
1.什么是HashMap?
是基于哈希表的Map接口的实现,HashMap是一个根据Hash算法存储键值对的一组集合(无序存储)。
查询和删除速度非常快。键值都可以存储为null。
键如果重复,则覆盖上一次put的键值对。这一点和HashTable则不同,HashTable不允许null键值对和重复的键值对。
HashMap是线程不安全的,HashTable是线程安全的。
2. HashMap重要参数
capacity:(初始容量)默认为1<<4(16),最大容量1<<30(1073741824)。当初始化容量大于 MAXIMUM_CAPACITY(最大容量)时,默认使用最大容量。
loadFactor:(负载因子)默认为0.75。
threshold: 阈值=capacity*loadFactor 该参数用于扩容。
其中capacity和loadFactor两个参数就决定了HashMap的性能,如果为了性能,就不要把初始化容量设置的太高(或把负载因子设置的太低)。
3.HashMapput方法源码解析
当我们往HashMap,put一个值时。
可以看见集合的容量(capacity)为16,负载因子(loadFactor)为0.75。阈值(threshold)为12=16×0.75。当size(元素数量)= threshold时,就会触发扩容机制。扩容的容量是当前容量×2,最大容量为1<<30=1073741824。
这里我们解释一下为什么存储键值对时不是按照队列存储的。
HashMap在put一个键值对时,首先会对键也就是key进行Hash算法,然后得出一个Hash值。根据这个Hash值来决定这个键值对放在哪个位置。这也就解释了为什么HashMap在存储键值对时是无序的。
了解过Hash算法的小伙伴应该也知道,不同的明文根据Hash算法得出的密文有可能是一样的。这种现象称为哈希碰撞。那么就会出现一种情况,两个不一样的key算出来的Hash值是一样的,这就会造成HashMap在存储键值对时发生冲突。我们来看看HashMap内部是如何解决这种情况的。
public V put(K key, V value) {
//调用putVal方法完成put
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否初始化,没有则执行初始化操作。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算键值对的存储位置,如果为空,则直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断该key是否存在,存在则直接赋值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断链表是否为红黑树
else if (p instanceof TreeNode)
//执行红黑树操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//添加链表
p.next = newNode(hash, key, value, null);
//如果链表的长度为8,将链表转化为红黑树存储
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断该key是否存在,存在则直接赋值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e代表了存在相同key的Node节点对象。如果e不等于空,则把旧的值覆盖为新的值。返回旧值,不执行size++
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//重新赋值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//集合修改次数
++modCount;
//判断是否需要扩容(当前size(键值对数量)+1>阈值)
if (++size > threshold)
//扩容方法
resize();
afterNodeInsertion(evict);//空操作
return null;
}
通过简单分析以上代码可以发现,在调用putVal方法之前,首先将key进行Hash计算。
在putVal方法中判断table是否执行了初始化操作。
然后计算出键值对存储的位置,如果为空,则直接进行赋值。
反之,如果不为空,则代表出现了哈希碰撞。通过Node节点中的next属性进行添加链表操作。并且在链表容量达到8时,将链表转化为红黑树存储。
如图(画的有点垃圾,勿喷。)
4.通过HashMap的构造函数来修改负载因子(loadFactor)所带来的影响
测试代码:
默认值
负载因子(loadFactor)为0.4
这里是以100万键值对进行测试。
相对的,loadFactor减少的同时,put花费的时间也增加了。个人认为,当数据量越大时,loadFactor越小并不能代表性能越快。性能快的主要原因就是HashMap容器中所生成的链表越少越好,也就是哈希碰撞越少越好。当然,如果造成大量的空间浪费,也是得不偿失的。
结束语
以上就是我的HashMap底层原理总结。欢迎各位大佬指点!有什么说的不对的地方也希望大家在下方评论区发言。