"HashMap?不就是一个数组加链表吗?" ------ 如果你还这样想,恭喜你,面试官已经准备好十连暴击了!
第一问:HashMap的底层结构是什么?
初级回答:"数组+链表,JDK8以后还有红黑树"
高手回答:
java
arduino
// 底层结构演进
JDK7:Entry[] table + 单向链表
JDK8:Node[] table + 单向链表/红黑树
JDK16以后:Node[] table + 单向链表/红黑树(但做了很多性能优化)
// 关键参数
static final int TREEIFY_THRESHOLD = 8; // 树化阈值
static final int UNTREEIFY_THRESHOLD = 6; // 链化阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量
第二问:HashMap的hash算法是怎么设计的?
面试官追问:"为什么用(h = key.hashCode()) ^ (h >>> 16)?"
核心思想:高位参与运算,减少hash冲突
java
yaml
// 直观理解:让hash值的高16位也参与运算
// 假设hashCode为:0000 0100 1011 0011 1101 1111 1110 0001
// 右移16位: 0000 0000 0000 0000 0000 0100 1011 0011
// 异或结果: 0000 0100 1011 0011 1101 1011 0101 0010
// ↑ 高位信息被保留到低位,增加散列性
// 计算索引:(n-1) & hash
// 数组长度通常不大,只有低位参与运算,所以要让高位也影响低位
第三问:HashMap什么时候扩容?扩容机制是怎样的?
触发条件:
- 元素数量 > 容量 × 加载因子(0.75)
- 链表长度 > 8,但数组长度 < 64(先扩容而不是树化)
扩容过程:
java
scss
// JDK7:头插法(可能导致死链)
void transfer(Entry[] newTable) {
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); // 重新计算位置
e.next = newTable[i]; // 头插法
newTable[i] = e;
e = next;
}
}
}
// JDK8:尾插法 + 巧妙的位置计算
// 元素在新数组中的位置要么是原位置,要么是原位置+旧容量
// 因为扩容是2倍,通过hash & oldCap判断高位
if ((e.hash & oldCap) == 0) {
// 位置不变
} else {
// 位置 = 原位置 + 旧容量
}
第四问:为什么加载因子是0.75?
数学与工程的平衡:
- 加载因子太小:空间浪费,比如0.5时数组只用了一半
- 加载因子太大:hash冲突增加,查询效率降低
- 0.75是基于泊松分布的数学计算得出的空间与时间的最优平衡点
第五问:红黑树转换的阈值为什么是8?
泊松分布告诉你答案:
java
markdown
// HashMap源码中的注释说明:
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
// 链表长度达到8的概率只有千万分之六
// 树化是一种极端情况的保护措施,不是常态
第六问:HashMap是线程安全的吗?会遇到什么问题?
致命三连击:
- 死循环:JDK7扩容头插法导致(JDK8已修复)
- 数据丢失:多线程put覆盖值
- size不准:并发更新计数器
演示代码:
java
ini
// 并发问题复现
public class HashMapConcurrentIssue {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> map = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Size: " + map.size()); // 可能小于2000
}
}
第七问:HashMap与HashTable、ConcurrentHashMap的区别?
对比分析:
特性 | HashMap | HashTable | ConcurrentHashMap |
---|---|---|---|
线程安全 | 否 | 是(全表锁) | 是(分段锁/CAS) |
Null键值 | 允许 | 不允许 | 不允许 |
性能 | 高 | 低 | 中等偏高 |
版本 | 1.2+ | 1.0+ | 1.5+ |
ConcurrentHashMap演进:
- JDK7:Segment分段锁
- JDK8:CAS + synchronized(锁粒度更细)
第八问:HashMap的key为什么要重写equals和hashCode?
经典面试题:
java
typescript
public class KeyExample {
private String name;
// 不重写hashCode:不同的KeyExample对象hashCode不同
// 不重写equals:默认用==比较
@Override
public int hashCode() {
return Objects.hash(name); // 相同name返回相同hashCode
}
@Override
public boolean equals(Object obj) {
// 相同name认为相等
}
}
// 使用场景
Map<KeyExample, String> map = new HashMap<>();
KeyExample key1 = new KeyExample("abc");
KeyExample key2 = new KeyExample("abc");
map.put(key1, "value1");
System.out.println(map.get(key2)); // 不重写返回null!
第九问:HashMap的遍历方式有哪些?性能如何?
四种遍历方式对比:
java
arduino
Map<String, Integer> map = new HashMap<>();
// 1. EntrySet遍历(推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
// 2. KeySet遍历(需要二次查找)
for (String key : map.keySet()) {
Integer value = map.get(key); // 多一次hash计算
}
// 3. forEach(JDK8+)
map.forEach((k, v) -> System.out.println(k + ": " + v));
// 4. 迭代器
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
}
第十问:设计一个LRU缓存,如何基于HashMap实现?
LinkedHashMap的巧妙用法:
java
typescript
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder=true按访问顺序排序
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 超过容量移除最老的
}
// 使用示例
public static void main(String[] args) {
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("1", "A");
cache.put("2", "B");
cache.put("3", "C");
cache.get("1"); // 访问1,让它变"新"
cache.put("4", "D"); // 2被淘汰(最老且最近没访问)
}
}
🎯 面试生存指南
当你被问懵时:
- "这个问题在JDK不同版本中有差异,我熟悉的是JDK8的实现..."
- "HashMap的设计确实很精妙,虽然我不记得具体阈值,但理解它的设计思想..."
- "在实际项目中,我们更关注如何正确使用HashMap,比如..."
展现深度:
- 提到源码中的关键常量定义
- 对比不同JDK版本的实现差异
- 结合实际项目中的使用场景和坑
战绩统计:
- 撑过3问:基础合格 ✅
- 撑过6问:准备充分 👍
- 撑过8问:源码级大佬 🚀
- 10问全通:HashMap是你家开的吧? 🤯
评论区挑战:你倒在了第几问?哪个问题最难?分享你的面试经历!