HashMap是Java中常用的实现了Map接口的哈希表数据结构。它基于哈希算法实现,提供了高效的插入、删除和查找操作。HashMap以键值对(key-value)的形式存储数据,其中键是唯一的,值可以重复。
HashMap存储数据的原理大致是:
- 计算哈希值:对键对象调用其hashCode()方法来获取哈希值。hashCode()方法是Object类的方法,可以被所有Java对象继承和覆盖。哈希值是一个整数,用于表示键的唯一标识。
- 确定索引位置:使用哈希值和当前数组的长度进行计算,以确定键值对应该放置在数组的哪个索引位置。通常,这是通过对哈希值进行模运算(取余操作)来实现的。具体计算方式为:索引位置 = 哈希值 % 数组长度。
- 哈希冲突:如果发生了哈希冲突,即不同的键对象计算得到相同的哈希值,那么它们将会被放置在同一个索引位置上,以形成一个链表或树的数据结构。
详细流程如下:
- 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
- 如果数组是空的,则调用 resize 进行初始化;
- 如果没有哈希冲突直接放在对应的数组下标里;
- 如果冲突了,且 key 已经存在,就覆盖掉 value;
- 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
- 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
常用的方法
V get(Object key) | 根据键获取对应的值 |
V put(K key, V value) | 将指定的键值对添加到HashMap中 |
V remove(Object key) | 根据键移除对应的键值对,并返回被移除的值 |
boolean containsKey(Object key) | 判断HashMap中是否包含指定的键 |
boolean containsValue(Object value) | 判断HashMap中是否包含指定的值 |
int size() | 返回HashMap中键值对的数量 |
boolean isEmpty() | 判断HashMap是否为空 |
void clear() | 清空HashMap,移除所有的键值对 |
Collection<V> values() | 返回HashMap中所有值的集合(Collection) |
Set<Map.Entry<K, V>> entrySet() | 回HashMap中所有键值对的集合(Set) |
Set<K> keySet() | 返回HashMap中所有键的集合(Set) |
存储结构:
1.7版本:
在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
1.8之后
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换。
- 当链表超过 8 且数据总量超过 64 才会转红黑树。
- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
为什么链表转红黑树的阈值是8?
hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此时的链表性能已经已经很差了,操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。
为什么红黑树转链表的阈值是6?
如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果HashMap不停的插入,删除元素,链表个数在8左右徘徊,就会频繁的发生红黑树转链表,链表转红黑树,效率会很低下。
为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。