代码基于 JDK 17。
HashMap
HashMap 的特性
- 添加和查找值的效率很高,都是 ,根据 hash 值可以快速定位
- 键值对没有顺序(因为 hash 值是随机的)
- 允许键或者值是null,线程不安全(如果需要线程安全,可以使用 Hashtable,但是 Hashtable 的键值不可以是 null)
Map 接口
int size(); // Map 中键值对的个数
boolean isEmpty(); // 是否为空
boolean containsKey(Object key); // 是否包含某个键
boolean containsValue(Object value); // 是否包含某个值
V get(Object key); // 根据键获取值,如果没有找到,返回 null
V put(K key, V value); // 保存键值对,如果 key 已经存在,覆盖原来的值,并把这个值返回
V remove(Object key); // 根据键删除键值对,返回 key 原来的值,如果不存在,返回 null
void putAll(Map<? extends K, ? extends V> m); // 保存 m 中所有键值对到当前 map
void clear(); // 清空 Map 中的键值对
Set<K> keySet(); // 获取 Map 中 键的集合
Collection<V> values(); // 获取 map 中所有值的集合
Set<Map.Entry<K, V>> entrySet(); // 获取 map 中所有键值对
interface Entry<K, V> { // 表示键值对的接口
K getKey(); // 键值对的键
V getValue(); // 键值对的值
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
Java 8 增加了方法,getOrDefault
,forEach
, replaceAll
, putIfAbsent
, remove(Object key, Object value)
, replace
, computeIfAbsent
, computeIfPresent
, compute
, merge
等。Java 9 增加了多个重载的 of
方法,Java 10 增加了一个 copyOf
方法,可以根据一个或多个键值对构建不变的 Map。
注:keySet()
,values()
和 entrySet()
返回的都是视图,因此在返回值上修改会修改 Map 本身。
实现原理
HashMap是 Map 的实现类,因此也沿用了 Map 的特性。有键和值的概念,一个键映射到一个值,按照键存储和访问值。键不能重复,如果给同一个键重复设置值,会覆盖键原来映射的值。
HashMap 使用了 Hash(哈希)。使用的是链地址法解决哈希冲突问题(哈希冲突的解决方式见附录)。Java 8 对 HashMap 进行了优化,在哈希冲突比较严重的情况下 ,即大量元素映射到同一个链表(链表中至少8个元素),将该链表转化为红黑树。
一、HashMap 内部组成
transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
table
是一个 Node(一个内部类,如下所示,JDK 7以前是 Entry类型)类型的数组,称为哈希表或者哈希桶,是实际用来存放键值对的地方。table
中每个元素指向一个单向链表(或者),每个节点表示一个键值对。
key
和 value
分别表示键和值,next
表示下一个节点,hash
是 key
的 hash 值。
size
是 Map 中实际存储的键值对的个数。threshold
指扩容的阈值,根据负载因子 loadFactor
计算。
modCount
是用来记录结构化的修改的次数(如键值对数量变化),方便在遍历 Map 时(迭代)检测是否有结构性变化。
二、初始化和扩容
public HashMap(int initialCapacity, float loadFactor)
初始化 HashMap 时,会对 loadFactor
和 initalCapacity
进行赋值,如果没有指定值,就会使用默认值。initialCapacity 指初始容量,默认为16,最大为 ;loadFactor
是负载系数,默认为0.75。
在添加第一个元素时,才会 对 Map 分配空间。
三、put() 保存键值对(添加 / 修改)
基本步骤如下
- 计算键的哈希值
- 根据哈希值得到保存的位置( )
- 查到对应位置的链表尾或者更新已有值
- 如果需要,扩展 table 大小
1. 计算 key 的 hash 值
如果 key 为 null,返回 0,否则对 key 的 hashCode 进行高低位异或运算并返回
2. 添加 / 修改键值对
判断键是否存在。先比较 hash 值,hash 值相同时,再用 equals
方法进行比较。(因为 hash 是整数,性能一般比 equals
高。如果 hash 不同,就无需 equals
了。)
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
四、get() 查找方法
调用 getNode()
获取对应节点,如果不存在,直接返回 null
,存在的话,获取值。
containsKey()
逻辑类似
containsValue()
遍历table中的链表的每个节点
五、remove() 删除键值对
逻辑和
get()
类似,
- 计算键的哈希值
- 根据哈希值得到保存的位置( )
- 如果查找到该点
- 如果是树节点,直接在红黑树中删除该节点
- 如果是待删节点是头节点,把头节点变成 next
- 如果是链表中的点,那么跳过这个点(删除该节点)
HashSet
HashSet 是 Set 的实现类,因此具有 Set 的特性。Set 是用来存储没有重复的元素。如果使用HashSet存储自定义类,需要重写 hashCode 和 equals 方法。
HashSet 的应用场景
- 需要对元素进行排重,且对元素没有顺序要求
- 保存特殊值
- 集合运算
Set 接口
扩展了 Collection
。Java 9 增加了多个重载的 of
方法,Java 10 增加了一个 copyOf
方法,可以根据一个或多个键值对构建不变的 Set。
实现原理
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
HashSet 内部实际是 HashMap。HashSet中的值相当于键,值都是相同的固定值 PRESENT
。
添加,就是调用put方法,如果map中不含这个键,则 put 返回 null。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
查找是否包含元素
public boolean contains(Object o) {
return map.containsKey(o);
}
删除,返回值为 PRESENT
表示原来有对应的键,并且删除成功。
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
迭代器,就是返回 map 的 keySet() 迭代器。
public Iterator<E> iterator() {
return map.keySet().iterator();
}
附录
哈希冲突
解决哈希冲突的几种方法:
-
开放定址法 如果发生哈希冲突,寻找一个新的空闲位置放置元素
- 线性探测法 如果发生冲突,就往后面依次寻找,直到找到空闲位置放置元素
- 平方探测法 如果发生冲突,就往前后面分别寻找
-
再哈希法 同时构造多个哈希函数,如果发生哈希冲突,就是用其他哈希函数计算地址
-
链地址法 将所有哈希地址相同的元素都放在同一个链表中