HashMap夺命十连问,你能撑到第几轮?

"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是线程安全的吗?会遇到什么问题?

致命三连击

  1. 死循环:JDK7扩容头插法导致(JDK8已修复)
  2. 数据丢失:多线程put覆盖值
  3. 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是你家开的吧? 🤯

评论区挑战:你倒在了第几问?哪个问题最难?分享你的面试经历!

相关推荐
每天进步一点_JL2 小时前
🔥 一个 synchronized 背后,JVM 到底做了什么?
后端
云动雨颤2 小时前
程序出错瞎找?教你写“会说话”的错误日志,秒定位原因
java·运维·php
魔芋红茶2 小时前
RuoYi 学习笔记 3:二次开发
java·笔记·学习
杨杨杨大侠2 小时前
Atlas Mapper 教程系列 (8/10):性能优化与最佳实践
java·spring boot·spring·性能优化·架构·系统架构
SamDeepThinking2 小时前
有了 AI IDE 之后,为什么还还要 CLI?
后端·ai编程·cursor
yinke小琪2 小时前
线程池七宗罪:你以为的优化其实是在埋雷
java·后端·面试
-雷阵雨-2 小时前
数据结构——包装类&&泛型
java·开发语言·数据结构·intellij-idea
我不是混子2 小时前
Spring Boot启动时的小助手:ApplicationRunner和CommandLineRunner
java·后端
用户723905105692 小时前
Java并发编程原理精讲
后端