16、常见面试题
1、HashMap和HashTable的区别
区别 | Hashtable | HashMap |
---|---|---|
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树(JDK 1.8+) |
是否可为null | Key 和 Value 都不能为 null(否则抛出 NPE) | Key 和 Value 均可为 null(但 Key 只能有一个 null) |
Hash算法 | 直接使用 key.hashCode() | 二次 Hash 计算((h = key.hashCode()) ^ (h >>> 16) ) |
扩容方式 | 扩容时容量翻倍后 +1(如原容量 10,新容量为 21) | 直接翻倍(如原容量 16,新容量为 32) |
线程安全 | 同步(synchronized 方法),线程安全但性能低 | 非线程安全,性能高,多线程用 ConcurrentHashMap 替代 |
总结:
-
开发建议:不建议使用
Hashtable
,多线程场景优先使用ConcurrentHashMap
。 -
功能差异:
HashMap
在哈希算法、数据结构、扩容机制上更优化,支持 null 键值。
2、HashSet和HashMap的区别
-
接口与用途
-
HashSet 实现
Set
接口,存储唯一元素。 -
HashMap 实现
Map
接口,存储键值对。
-
-
内部实现
-
HashSet 内部封装了一个 HashMap,元素作为键,值统一为
PRESENT
(一个静态 Object 对象)。 -
HashMap 直接管理键值对,通过哈希函数计算键的存储位置。
-
-
方法差异
-
HashSet 的
add()
方法调用 HashMap 的put()
方法,插入元素作为键。 tips: hashCode和equals结果一致的才算相同元素。 -
HashMap 通过
put()
存储键值对,需处理哈希冲突和可能的键覆盖。
-
3、HashMap的实现原理
-
数据结构 JDK 1.8 后采用 数组 + 链表 + 红黑树:
-
默认数组长度 16,负载因子 0.75。
-
链表长度 >8 时转为红黑树(树化),红黑树节点数 <6 时退化为链表。
-
-
哈希冲突解决
-
通过
hashCode()
计算键的哈希值,再对数组长度取模确定索引。 -
哈希冲突时,冲突的键值对以链表或红黑树形式存储。
-
获取时,直接找到hash值对应的索引,在判断是否有重复的key值,然后输出。
-
-
Hash 计算优化 JDK 1.8 的哈希算法:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
,减少碰撞概率。
4、HashMap在JDK1.7和JDK1.8中有什么区别
区别点 | JDK 1.7 | JDK 1.8 |
---|---|---|
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
插入方式 | 头插法(可能产生循环链表) | 尾插法(避免循环链表) |
哈希算法 | 多次扰动(4次位运算+5次异或) | 1次位运算 + 1次异或 |
扩容机制 | 重新计算所有节点的位置 | 红黑树拆分成的树的节点数<=临界值(6个)则退化为链表 |
-
在1.8后:当链表长度大于阈值(默认为8)时并且数组长度达到64时,将链表转化为红黑树
5、HashMap的put方法的具体流程
-
初始化检查
-
若数组
table
为空或为null
,调用resize()
方法进行初始化(默认容量 16)。
-
-
计算哈希值与索引
-
根据键
key
的hashCode()
计算哈希值,并通过扰动函数优化(如(h = key.hashCode()) ^ (h >>> 16)
)。 -
计算数组索引:
索引 = (数组长度 - 1) & hash
。
-
-
处理空桶
-
若
table[i]
为空,直接创建新节点并插入。
-
-
处理非空桶(哈希冲突)
-
Key 相同:若
table[i]
的首个节点与当前key
相同(equals()
为 true),直接覆盖其值。 -
红黑树插入:若
table[i]
是红黑树节点(TreeNode
),调用红黑树的插入方法。 -
链表插入:
-
遍历链表,若发现
key
已存在则覆盖值。 -
若未找到相同
key
,则在链表尾部插入新节点。 -
插入后检查链表长度:若链表长度 ≥8 且数组长度 ≥64,将链表转为红黑树。
-
-
-
扩容检查
-
插入成功后,若键值对总数
size
超过阈值(容量 × 负载因子
,默认 0.75),调用resize()
扩容。
-
6、HashMap的扩容机制
-
触发条件
-
元素数量 > 当前容量 × 负载因子(默认 0.75)。
-
初始化时未指定容量,首次插入数据时触发初始化扩容(默认到 16)。
-
-
扩容步骤
-
新容量 = 旧容量 × 2,新阈值 = 新容量 × 负载因子。
-
遍历旧数组,重新分配每个节点到新数组:
-
单节点:直接按新索引(
hash & (newCap-1)
)插入。 -
链表或树:根据
hash & oldCap
判断节点应留在原位置还是移动到原位置 + oldCap
。
-
-
-
JDK 1.8 优化
-
无需重新计算哈希值,利用高位判断新位置。
-
链表拆分时保持原顺序,避免并发环境下死链问题。
-