HashMap:内部组成&put:、get、remove方法大致逻辑&总结
源码基于java1.8
一、传统 HashMap的缺点
(1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
(2)当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题
引用自:https://blog.****.net/lianhuazy167/article/details/66967698
若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。
因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
引用自:https://blog.****.net/xingfei_work/article/details/79637878
HashMap主要有以下几个实例变量:
transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;
PS:transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。
table是一个Node类型的数组,称为哈希桶或哈希表,其中每个元素指向一个单向链表,链表中每一个节点表示一个键值对。Node是一个内部类,他的实例变量和构造方法如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
1、hash是key的hash值,存储hash值是为了加快计算。
2、添加第一个元素时默认分配大小为16,但并不是size大于16时再进行扩展,什么时候扩展和threshold有关
3、threshold标识阈值,当size(键值对个数)大于等于threshold时考虑进行扩展。
4、loadFactor是负载因子,表示整体上table被占用的成都,是一个浮点数,默认为0.75,可以通过构造方法进行修改;
PS:一般来说threshold=table.length(默认16)*loadFactor
put方法大致逻辑(该逻辑基于jdk1.7)
1.8的逻辑稍微复杂可参考上面的文章
这里借鉴网上的一张图,对于1.8的逻辑描述:
get方法(jdk1.8)
关于get和put的方法详细介绍可以参考文首引用的文章,写的相当不错
remove方法(jdk1.8)
该部分转自:https://blog.****.net/weixin_42340670/article/details/81139900
/**
* 从HashMap中删除掉指定key对应的键值对,并返回被删除的键值对的值
* 如果返回空,说明key可能不存在,也可能key对应的值就是null
* 如果想确定到底key是否存在可以使用containsKey方法
*/
public V remove(Object key) {
Node<K,V> e; // 定义一个节点变量,用来存储要被删除的节点(键值对)
return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value; // 调用removeNode方法
}
/**
* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述)
*
* @param hash key的hash值,该值是通过hash(key)获取到的
* @param key 要删除的键值对的key
* @param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
* @param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
* @param movable 删除后是否移动节点,如果为false,则不移动
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index; // 声明节点数组、当前节点、数组长度、索引值
/*
* 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
* 需要从该节点p向下遍历,找到那个和key匹配的节点对象
*/
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
// 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
/*
* 到这一步说明首节点没有匹配上,那么检查下是否有next节点
* 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
* 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
*/
else if ((e = p.next) != null) {
// 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可
else {
do {
// 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// 走到这里,说明e也没有匹配上
p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
} while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
}
}
/*
* 如果node不为空,说明根据key匹配到了要删除的节点
* 如果不需要对比value值 或者 需要对比value值但是value值也相等
* 那么就可以删除该node节点了
*/
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
p.next = node.next;
++modCount; // HashMap的修改次数递增
--size; // HashMap的元素个数递减
afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
return node;
}
}
return null;
}
PS:(length-1) & hash 能得到key在table中的位置;因为length为2的幂次方 (length-1) & hash等同于求模运算h%length.
小结:
HashMap存取时都根据键的hash值,只在对应的链表中操作,不访问别的链表,在对应的链表进行操作时也是线比较hash值,如果hash值相同再用equals进行比较。这就要求,相同对象的hashCode返回值必须相同;
1、因为内部使用链表,红黑树,和哈希值的方式实现,所以保存和取值的效率都很高,为O(1),每个单向链表往往只有一个或少数节点,根据hash值就可以快速定位;
2、HashMap只能工的键值对没有顺序,因为hash值是随机的
如果经常存取,且不要求顺序,那么HashMap是理想的选择。如果要求保持添加顺序,可以使用LinkedHashMap或TreeMap
3、 HashMap不是线程安全的,Java中还有一个Hashtable,他是java早起的容器类之一,实现了map接口,原理和HashMap类似,但没有特别的优化,它内部同事synchronized实现了线程安全。但是高并发场场景中推荐使用ConcurrentHashMap。
好了写的很多了,大家有什么指导或疑问欢迎评论沟通