前置准备
Map是什么
什么是Map呢?Map就是用来存储键值对的接口,注意,它与Collection的区别。Collection存储的是单列数据,Map存储的键值对。什么叫键值对。就是一个key,一个value。
在Map中,每个键(key)都是唯一的,通过键可以找到对应的值(value)。Map接口不是Collection接口的直接子接口,但它与Collection接口的实现类(如List和Set)一起构成了Java集合框架的核心部分。
我们举一个生活中的例子,我们把一些常见的公共电话与它的功能做一个对应。
比如 110 是警察局
120 是医院
119 是火警
122 是车辆救援
12345 是消费者权益保护。用一个键,可以快速获取一个值
Map的使用,一句话。就是你怎么需要key,value。 Map就可以变成你想要的。
什么是Hash
Hash(也称为哈希或散列)在计算机科学中是指一个过程,通过这个过程,任意大小的数据(通常称为预映射或输入)被转化为固定长度的输出,这个输出被称为哈希值或散列。哈希函数是用于执行这个转换的算法,它的设计目标是使得相同的输入总是产生相同的输出,而不同的输入产生不同的输出(理想情况下是唯一的输出)。
- 确定性:相同的输入总是产生相同的输出。
- 不可逆性:从哈希值恢复原始输入通常是不可能的,因为哈希函数是单向的。
- 冲突的最小化:虽然不可能保证不同的输入总是产生不同的哈希值,但好的哈希函数应该尽量减少哈希冲突,即不同输入产生相同哈希值的概率。
HashMap的底层结构
HashMap在Java中的底层实现是基于数组和链表(在JDK 8及以后版本中还包括红黑树)的数据结构。这种组合称为“拉链法”或“开放寻址法”,其核心思想是:
- 数组:HashMap内部维护一个Entry对象数组,这个数组的大小通常是2的幂次方,例如16、32、64等,以方便通过位运算快速计算索引。数组的大小可以在需要时动态调整。
- 链表:当多个键通过哈希函数映射到数组的同一个位置时,这些键值对不会直接覆盖,而是通过链表连接在一起。这样可以解决哈希碰撞问题,保证每个键值对都能存储。
- 红黑树:在JDK 8中,为了提高查找、插入和删除的效率,当某个桶(数组位置)的链表长度达到一定阈值(通常是8)时,链表会被转换成红黑树。红黑树是一种自平衡的二叉查找树,可以保证插入、删除和查找的时间复杂度接近O(log n)。
下面是HashMap工作流程的简要概述:
- 当插入一个键值对时,首先计算键的哈希值。
- 使用哈希值和数组大小进行模运算,得到在数组中的索引位置。
- 如果该位置是空的,键值对直接插入。
- 如果位置已经有其他键值对,就将新的键值对添加到链表的末尾(或红黑树中)。
- 如果链表过长,会将链表转换为红黑树,以优化查找性能。
- 当数组负载因子(已存储元素数量 / 数组大小)达到默认的0.75时,会进行扩容,通常数组大小翻倍,然后重新哈希所有的键值对到新的数组中,以保持负载均衡。
为什么HashMap的底层,是数组+链表+红黑树?
把添加的流程答出来,然后分析,链表是必不可少的(因为存在多个key落在同一个数组位置)。如果链表长度太长,这时候,效率太差,采用红黑树提高效率。
数组:
- 快速访问:数组提供通过索引直接访问元素的能力,这使得查找、插入和删除的时间复杂度可以达到O(1)的平均期望。
- 内存连续:数组的元素存储在内存中是连续的,有利于CPU缓存的效率。
链表:
- 解决哈希碰撞:哈希函数不可能完美地将所有键均匀分布,所以当多个键映射到同一索引时,链表用于链接这些键值对,避免丢失数据。
- 简单灵活:链表的插入和删除操作不需要移动元素,只需更改指针,因此在哈希碰撞较多时依然有效。
红黑树:
- 性能优化:当链表长度增长到一定程度(在Java 8中是8),链表的查找、插入和删除操作的平均时间复杂度会退化为O(n)。为了改善这一点,引入了红黑树,其插入、删除和查找操作的平均时间复杂度为O(log n),即使在冲突严重的区域也能保持高效。
- 平衡性:红黑树是一种自平衡的二叉查找树,能够保证树的高度相对较小,从而保证操作的效率。
HashMap的特点
HashMap对重复的key的定义
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key1", "newValue1");
HashMap如何判断相等
(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
HashMap重写判定相等的方法
public class Person {
private String name;
private int age;
// 构造方法、getter和setter省略...
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
链表什么时候转化为红黑树
当某个下标位置, 链表长度, 超过8达到9个时候(算上新加的结点), 就要由链表转化为红黑树。
当链表数目从8到达9,一定会转化为红黑树吗?
不一定会
如果数组的长度小于64.直接resize 扩容。如果数组长度, 小于64, 即使某个下标位置,链表长度已经超过8, 达到9了, 不会转化为红黑树, 而是扩容, 扩容会导致原本存在于这个位置的数据, 拆成两部分
红黑树转化为链表
有两个情况:
第一个情况, 删除数据的时候; 要删除的数据在红黑树上, 删除数据导致红黑树上数据量变少, 由红黑树转化为链表第二个情况: 扩容的时候, 一个红黑树再扩容之后, 被拆成两部分, 任一部分数据量过少, 也会由红黑树转化为链表
红黑树拆成低位(旧位置)和高位(旧位置+旧数组长度: 新位置)两部分, 这两部分, 任何一部分分配的数据量小于等于6个, 就要由红黑树转化为链表
HashMap的添加过程
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}