HashMap工作原理
HashMap 是 Java 集合框架中最重要且最常用的数据结构之一,它基于哈希表实现了 Map 接口,提供了高效的键值对存储和检索能力。
Java 8 之后的 HashMap 采用 数组 + 链表 + 红黑树 的混合结构:
// 简化结构示意
transient Node<K,V>[] table; // 主数组(哈希桶数组)static class Node<K,V> { // 链表节点final int hash;final K key;V value;Node<K,V> next;
}static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { // 红黑树节点TreeNode<K,V> parent; TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // 保持双向链表特性boolean red;
}
一、工作流程
1. 存储过程(put)
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
具体步骤:
计算哈希值:对 key 进行 hash(key) 计算(高16位异或低16位)
确定桶位置:index = (n - 1) & hash(n 是数组长度)
处理碰撞:
如果桶为空:直接创建新节点插入
如果桶不为空:
如果是链表:遍历查找,存在则更新,不存在则尾插(Java8),检查是否需树化(链表长度≥8)
如果是红黑树:按照树的方式插入
扩容检查:如果元素总数超过阈值(容量×负载因子),进行扩容(2倍)
2. 读取过程(get)
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}
具体步骤:
计算 key 的哈希值
确定桶位置 (n-1) & hash
在桶中查找:
如果是链表:顺序查找(O(n))
如果是红黑树:树查找(O(log n))
3. 扩容机制(resize)
当 size > threshold(容量×负载因子)时触发:
创建新数组(2倍于原容量)
重新计算所有元素的哈希位置(rehash)
迁移元素:
Java 7:头插法(可能导致死链)
Java 8+:保持原链表顺序或拆分树
二、线程安全问题
HashMap 不是线程安全的,多线程环境下可能出现:
数据不一致
Java 7 扩容时的死循环问题(已修复)
解决方案:
使用 Collections.synchronizedMap
使用 ConcurrentHashMap(推荐)
三、常见问题
1、HashMap的value是否可以传null?
HashMap允许null作为值存储在HashMap中。允许一个null作为key,如果尝试使用多个null作为key,后面的会覆盖前面的。
2、HashMap和Hashtable的区别
HashMap线程不安全,Hashtable线程安全
HashMap允许null键值,Hashtable不允许
HashMap性能更好
3、HashMap 的初始容量和负载因子是什么?
默认初始容量 16
默认负载因子 0.75
当元素数量超过(容量×负载因子)时扩容
4、HashMap 如何解决哈希冲突?
哈希冲突是指不同的键(key)经过哈希函数计算后得到相同的哈希值,从而映射到哈希表的同一个位置。HashMap 主要采用以下几种方式解决哈希冲突:
- 链地址法(拉链法)Java 7 及之前版本的实现方式:
使用数组+链表的结构
当发生哈希冲突时,将冲突的键值对以链表形式存储在同一个桶(bucket)中
新元素插入到链表头部(头插法)
// Java 7 的简单示意结构
数组 + 链表:
[0] -> null
[1] -> Entry<K,V> -> Entry<K,V> -> null // 哈希冲突的键值对形成链表
[2] -> null
...
- 红黑树优化(Java 8+)
当链表长度超过阈值(默认为8)时,将链表转换为红黑树
当红黑树节点数小于阈值(默认为6)时,转换回链表
这种改进将最坏情况下的时间复杂度从O(n)降低到O(log n)
// Java 8+ 的混合结构
[0] -> null
[1] -> TreeNode<K,V> // 转换为红黑树
[2] -> Node<K,V> -> Node<K,V> -> null // 仍然是链表
...
5、为什么 HashMap 的长度是 2 的幂次方?
方便通过位运算计算索引:(n-1) & hash
提高计算效率,减少哈希冲突
6、HashMap 的 put 方法执行流程?
- 计算 key 的 hash 值
- 计算数组下标
- 判断是否冲突
- 插入节点(链表或红黑树)
7、为什么 Java 8 要将链表转为红黑树?
防止哈希碰撞攻击
链表过长时查询效率从 O(n) 提升到 O(logn)
8、HashMap 为什么不是线程安全的?
多线程扩容可能导致死循环
使用 ConcurrentHashMap 替代
9、HashMap 的扩容机制是怎样的?
扩容为原大小的 2 倍
重新计算所有元素的位置
10、如何优化 HashMap 的性能?
设置合理的初始容量
选择合适的负载因子
使用不可变对象作为键
11、HashMap 和 TreeMap 的区别?
HashMap 基于哈希表,无序
TreeMap 基于红黑树,有序
12、HashMap 在多线程环境下会出现什么问题?
数据不一致
死循环(Java 7 及以前版本)
推荐使用 ConcurrentHashMap
13、如何设计一个好的 hashCode 方法?
保证相同对象返回相同值
尽量使不同对象返回不同值
计算简单高效
14、HashMap 的 key 为什么通常用不可变对象?
防止 key 变化导致 hash 值变化
保证数据一致性
15、如何实现一个线程安全的 HashMap?
使用 Collections.synchronizedMap
使用 ConcurrentHashMap
使用读写锁封装