1. Java 集合框架的整体结构

集合分两大体系:

🎯 面试要点

  • HashSet 底层就是 HashMap(value 存固定 Object);TreeSet 底层是 TreeMap
  • Vector 已过时(方法全加锁),单线程用 ArrayList,并发用 CopyOnWriteArrayList
  • Hashtable 已过时,并发 Map 用 ConcurrentHashMap

2. ArrayList 和 LinkedList 的区别?

ArrayList:底层动态数组(Object[] elementData)。

LinkedList:底层双向链表(Node prev/data/next,且实现 Deque)。

结论:99% 场景用 ArrayList(局部性原理,缓存友好)。LinkedList 的"插入快"在随机插入时并不成立,因为要先遍历找位置。

🎯 面试要点

  • 扩容计算:oldCapacity + (oldCapacity >> 1) = 1.5 倍(JDK8+)
  • ArrayList 的 subList 视图与 removeAll 等的坑:subList 结构修改会影响原 list
  • 频繁头部插入且需要双端操作 → 用 ArrayDeque 比 LinkedList 更快(数组版双端队列)

3. HashMap 底层原理?(面试必问中的必问)

数据结构(JDK8):数组 + 链表 + 红黑树。数组默认容量 16,每个槽位是链表头,链表长度 >= 8 且数组长度 >= 64 时转红黑树。

put 流程:

  1. 计算 key 的 hash:h = key.hashCode() ^ (h >>> 16)(高 16 位与低 16 位异或,让高位也参与寻址,降低碰撞)
  2. 定位桶:index = (n - 1) & hash(等价于 hash % n,因为 n 是 2 的幂)
  3. 桶为空 → 直接放;不为空 → 遍历链表比较 equals;key 已存在 → 覆盖 value;否则尾插法(JDK7 是头插,会形成死链)
  4. 链表长度达到 8 → 转红黑树(前提数组长度 ≥ 64,否则先扩容)
  5. size 超过 阈值 = 容量 × 加载因子 0.75 → 扩容 2 倍并 rehash 重排

为什么 JDK7 → JDK8 变化大:

为什么数组容量必须是 2 的幂?
// 定位桶:hash % n 可用位运算替代,前提 n 是 2 的幂
int index = (n - 1) & hash;

// n=16:   n-1 = 0000 1111(二进制)
// 结果只保留 hash 的低 4 位 → 均匀分布在 0~15
// 若 n=15(非 2 的幂): n-1 = 0000 1110,最后一位恒为 0
// → 奇数下标永远空着,浪费一半桶且碰撞加剧

🎯 面试要点

  • 默认容量 16、加载因子 0.75、树化阈值 8、退化为链表阈值 6(防止抖动)、最小树化容量 64
  • 为什么树化阈值是 8?泊松分布下链表长度到 8 的概率极低(约千万分之六),8 是"空间换时间"的平衡点
  • 重写 equals 必须重写 hashCode,否则 HashMap 找不到 key(见面向对象章节)
  • HashMap 线程不安全:数据覆盖、size 不准确、JDK7 还会死循环

4. ConcurrentHashMap 如何保证线程安全?

JDK7:分段锁(Segment 数组)。默认 16 个 Segment,每个 Segment 是一把小锁(继承 ReentrantLock),锁粒度是"段"。并发度 = 段数。

JDK8:抛弃分段锁,改为 CAS + synchronized 锁桶头节点:

JDK8 的改进思路:锁粒度从"段"细化为"桶",且空桶用 CAS 避免加锁开销,读读、读写并行度都更高。

🎯 面试要点

  • JDK8 移除了 Segment,源码结构向 HashMap 靠拢(数组+链表+红黑树)
  • 扩容是并发扩容:多线程协助迁移桶(transfer 方法),无锁迁移保证安全
  • 不允许 null key/value(避免二义性:无法区分"值不存在"与"值为 null")

5. TreeMap 和 LinkedHashMap 的特点?

TreeMap:底层红黑树,键有序。支持自然排序(Comparable)和自定义排序(Comparator)。操作 O(log n)。用于需要按 key 排序或范围查询的场景(如区间统计)。

LinkedHashMap:HashMap + 双向链表维护插入顺序。构造时可开启 accessOrder=true,链表按"访问顺序"维护 → 被访问的节点移到尾部。**这正好是实现 LRU 缓存的底层机制**(LinkedHashMap 设计目的之一)。

基于 LinkedHashMap 实现 LRU 缓存
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    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;   // 超过容量时淘汰最久未访问的
    }
}

🎯 面试要点

  • TreeMap 的 key 必须可比较(否则抛 ClassCastException)
  • LeetCode 146 LRU 缓存:标准解法 HashMap + 双向链表;面试官常追问"JDK 里有没有现成的"
  • 红黑树:自平衡二叉查找树,保证任何路径黑高相等 → 最坏 O(log n)

6. 什么是 fail-fast 和 fail-safe?

fail-fast(快速失败):迭代器遍历时若集合被结构性修改(增/删,不包括 set 修改值),立即抛 ConcurrentModificationException。实现原理:迭代器持有 modCount(修改次数),每次 next() 检查 modCount 是否被改动。

fail-safe(安全失败):遍历的是拷贝,修改原集合不影响遍历,不抛异常。代表:CopyOnWriteArrayList / ConcurrentHashMap 的迭代器。代价是遍历期间读不到最新数据。

经典踩坑:遍历中删除
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");

// ❌ 抛 ConcurrentModificationException
for (String s : list) {
    if (s.equals("b")) list.remove(s);
}

// ✅ 正确:使用迭代器的 remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("b")) it.remove();
}

🎯 面试要点

  • 增强 for 循环本质是迭代器 → 循环体内直接 list.remove() 会失败
  • modCount 是 int,被改后迭代器记录的值不一致即抛异常
  • 单线程下遍历删除必须用 Iterator.remove() 或倒序遍历 + remove

🎤 常见面试追问

  1. HashMap 为什么链表长度到 8 才转红黑树?——泊松分布下链表长度到 8 的概率约千万分之六,几乎不会发生;树化是"防恶意哈希碰撞"的兜底,且树节点占内存大(约普通节点 2 倍),阈值要平衡。
  2. ConcurrentHashMap 的 put 是怎么保证线程安全的?——桶空用 CAS 无锁插入;桶不空 synchronized 锁桶头节点;size 用 CounterCell 分散计数。
  3. fail-fast 的 modCount 是什么?——集合记录"结构性修改次数"的字段;迭代器创建时记下它,每次 next 检查,变了就抛 ConcurrentModificationException。
  4. HashMap 线程不安全会出什么问题?——数据覆盖丢失、size 不准确;JDK7 并发扩容还会形成环形链表导致 get 死循环(JDK8 尾插已修复但仍有覆盖问题)。
  5. 为什么加载因子是 0.75 而不是 1?——0.75 是"空间 vs 碰撞"的平衡:太高(如 1)碰撞多、树化频繁;太低浪费空间。0.75 是工程上验证的折中。

📖 名词解释(本页术语)

术语 大白话解释
ArrayList动态数组:按下标取值 O(1),尾插快,中间插入/删除要搬元素。容量不够自动扩容 1.5 倍。
LinkedList双向链表:任意位置插入删除 O(1)(找到节点后),按下标找 O(n)。内存比 ArrayList 大(每个节点多两个指针)。
HashMap键值对容器:键的哈希定位桶(数组),桶内链表/红黑树存冲突。查找 O(1)。线程不安全。
红黑树自平衡二叉搜索树:任何操作最坏 O(log n)。HashMap 桶内冲突超过 8 个时从链表转成它。
哈希冲突两个不同的 key 算出同一个桶位置。解决:链地址法(链表存一起)——HashMap 的做法。
ConcurrentHashMap线程安全的 HashMap(JDK8:CAS + synchronized 锁桶),读不加锁,并发度远高于 Hashtable。
fail-fast(快速失败)遍历时检测到集合被修改立即抛 ConcurrentModificationException——宁可报错,不产生错误结果。
加载因子触发扩容的"水位线":元素数 / 容量 ≥ 0.75 就扩容 2 倍。0.75 是空间与性能的平衡点。
TreeMap按键排序的 Map(红黑树实现),支持范围查询,O(log n)。key 必须可比较。
LinkedHashMapHashMap + 双向链表:维护插入顺序;accessOrder=true 时按访问顺序排列,可做 LRU 缓存。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。