以下是关于 HashMap 的常见面试题及其答案整理,涵盖底层原理、使用场景和优化技巧


1. HashMap 的底层数据结构是什么?

  • 答案
    JDK 1.8 之前:数组 + 链表(链表解决哈希冲突)。
    JDK 1.8 及之后:数组 + 链表/红黑树(当链表长度 ≥8 且数组长度 ≥64 时,链表转为红黑树,提高查询效率)。

2. HashMap 的工作原理(put/get 过程)?

put 过程

  1. 计算键的哈希值:hash(key) = (key == null) ? 0 : key.hashCode() ^ (hashCode >>> 16)(扰动函数减少哈希冲突)。
  2. 根据哈希值确定数组下标:index = (n - 1) & hashn 为数组长度)。
  3. 若该位置为空,直接插入键值对;否则遍历链表/红黑树:

    • 若键已存在,更新值;
    • 若键不存在,插入到链表尾部(或红黑树)。
  4. 插入后判断是否需要扩容。

get 过程

  1. 计算键的哈希值,确定数组下标。
  2. 遍历链表/红黑树,通过 equals() 方法匹配键,返回对应的值。

3. 为什么 HashMap 线程不安全?

  • 原因

    • 多线程扩容:扩容时链表可能形成环形结构,导致 get() 死循环(JDK 1.7 问题,1.8 已优化)。
    • 数据覆盖:多线程同时插入且哈希冲突时,可能覆盖已存在的键值对。
    • 可见性问题:未使用同步机制,修改对其他线程不可见。

4. HashMap 的哈希冲突如何解决?

  • 拉链法(链地址法):将冲突的键值对存储在链表或红黑树中。
  • 开放寻址法:HashMap 未使用,但其他类(如 ThreadLocal)可能采用。

5. 为什么链表长度超过 8 会转为红黑树?

  • 设计依据:根据泊松分布,哈希冲突时链表长度达到 8 的概率极低(约 1/千万次),此时转为红黑树可平衡查询效率与树化成本。
  • 退化条件:当红黑树节点数 ≤6 时,退化为链表。

6. HashMap 的初始容量、负载因子与扩容机制?

  • 默认初始容量:16。
  • 负载因子(Load Factor):默认 0.75(平衡时间与空间开销)。
  • 扩容条件:当元素数量 > 容量 × 负载因子时,扩容为原来的 2 倍。
  • 扩容过程

    1. 创建新数组(容量翻倍)。
    2. 重新计算键的哈希并分配到新位置(JDK 1.8 优化:链表节点保持原顺序,避免环形链表)。

7. HashMap 允许键/值为 null,而 ConcurrentHashMap 不允许?

  • HashMap:设计为单线程使用,允许 null 键值简化代码逻辑(例如表示“无值”)。
  • ConcurrentHashMap:多线程场景下,null 可能导致歧义(无法区分“键不存在”和“键存在但值为 null”)。

8. HashMap 与 Hashtable 的区别?

特性HashMapHashtable
线程安全是(方法用 synchronized 修饰)
null 键/值允许禁止
初始容量1611
扩容机制2 倍2 倍 +1
哈希计算扰动函数优化哈希分布直接使用 key.hashCode()

9. 如何设计一个高效的键类型(如自定义对象作为 Key)?

  • 重写 hashCode()equals():确保哈希分布均匀且相等对象返回相同哈希。
  • 示例

    @Override
    public int hashCode() {
        return Objects.hash(id, name); // 使用工具类生成复合哈希
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        MyKey other = (MyKey) obj;
        return id == other.id && Objects.equals(name, other.name);
    }

10. 如何解决 HashMap 的多线程问题?

  • 方案

    1. 使用 ConcurrentHashMap:分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8+)。
    2. Collections.synchronizedMap():包装 HashMap,但性能较低。
    3. 避免在多线程中直接操作 HashMap

11. HashMap 的遍历方式有哪些?

  • EntrySet 遍历:效率最高(直接获取键值对)。

    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        int value = entry.getValue();
    }
  • KeySet 遍历:需额外调用 get(),可能触发多次哈希计算。
  • Lambda 表达式(Java 8+)

    map.forEach((k, v) -> System.out.println(k + ": " + v));

总结

  1. 数据结构与红黑树转换
  2. 哈希冲突解决与扩容机制
  3. 线程安全与替代方案
  4. hashCode() 与 equals() 的设计
  5. JDK 1.7 与 1.8 的差异(如头插法改尾插法、树化优化)。

今夜有点儿凉
40 声望3 粉丝

今夜有点儿凉,乌云遮住了月亮。


引用和评论

0 条评论