这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
定义
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap底层实现原理解析
我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点: 1、数组结构: 存储区间连续、内存占用严重、空间复杂度大
优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)
缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。
2、链表结构:存储区间离散、占用内存宽松、空间复杂度小
优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活
缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)
3、哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构 常见的HashMap就是这样的一种数据结构
HashMap中的put()和get()的实现原理:
1、map.put(k,v)实现原理
- 首先将k,v封装到Node对象当中(节点)
- 然后它的底层会调用K的hashCode()方法得出hash值。
- 通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
2、 map.get(k)实现原理
- 先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
- 通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
hashmap的put实现方法
put函数大致的思路为:
- 对key的hashCode()做hash,然后再计算index;
- 如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 当数组table为null时, 调用resize生成数组table, 并令tab指向数组table
if ((p = tab[i = (n - 1) & hash]) == null) // 如果新存放的hash值没有冲突
tab[i] = newNode(hash, key, value, null); // 则只需要生成新的Node节点并存放到table数组中即可
else { // 否则就是产生了hash冲突
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果hash值相等且key值相等, 则令e指向冲突的头节点
else if (p instanceof TreeNode) // 如果头节点的key值与新插入的key值不等, 并且头结点是TreeNode类型,说明该hash值冲突是采用红黑树进行处理.
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 向红黑树中插入新的Node节点
else { // 否则就是采用链表处理hash值冲突
for (int binCount = 0;; ++binCount) { // 遍历冲突链表, binCount记录hash值冲突链表中节点个数
if ((e = p.next) == null) { // 当遍历到冲突链表的尾部时
p.next = newNode(hash, key, value, null); // 生成新节点添加到链表末尾
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果binCount即冲突节点的个数大于等于 (TREEIFY_THRESHOLD(=8) - 1),便将冲突链表改为红黑树结构, 对冲突进行管理,
// 否则不需要改为红黑树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 如果在冲突链表中找到相同key值的节点, 则直接用新的value覆盖原来的value值即可
break;
p = e;
}
}
if (e != null) { // 说明原来已经存在相同key的键值对
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent为true表示仅当<key,value>不存在时进行插入, 为false表示强制覆盖;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 修改次数自增
if (++size > threshold) // 当键值对数量size达到临界值threhold后, 需要进行扩容操作.
resize();
afterNodeInsertion(evict);
return null;
}