← 返回面试专题导航

📦 Java 集合框架面试题总览

掌握 List、Set、Map 的核心原理和使用场景

集合框架是 Java 开发的基础,也是面试的高频考点。本篇系统梳理集合的底层实现和常见陷阱。

🎯 难度筛选

📚 本篇覆盖内容

本篇重点围绕集合框架的核心知识,帮助你系统掌握:

  1. 集合体系:Collection、Map 两大体系的继承关系和核心接口。
  2. List 实现:ArrayList、LinkedList 的底层原理和性能对比。
  3. Set 实现:HashSet、TreeSet 的去重机制和排序原理。
  4. Map 实现:HashMap、TreeMap 的存储结构和线程安全问题。
  5. Queue/Deque:队列和双端队列的应用场景和实现选择。

🔗 相关专题

📋 Queue/Deque 详解 📦 List/Set/Map 对比 🗺️ HashMap 原理深度解析

一、集合框架概述

1. Java 集合框架的体系结构是怎样的? 简单
集合框架体系: Collection(单列集合) ├─ List(有序、可重复) │ ├─ ArrayList(动态数组) │ ├─ LinkedList(双向链表) │ └─ Vector(线程安全的动态数组) ├─ Set(无序、不可重复) │ ├─ HashSet(基于 HashMap) │ ├─ LinkedHashSet(保持插入顺序) │ └─ TreeSet(有序,基于红黑树) └─ Queue(队列) ├─ PriorityQueue(优先队列) └─ Deque(双端队列) Map(双列集合,键值对) ├─ HashMap(基于哈希表) ├─ LinkedHashMap(保持插入顺序) ├─ TreeMap(有序,基于红黑树) ├─ Hashtable(线程安全,已过时) └─ ConcurrentHashMap(线程安全)
💡 记忆口诀:Collection 单列,Map 双列;List 有序可重复,Set 无序不重复。
2. ArrayList 和 LinkedList 的区别?什么时候用哪个? 中等
ArrayList vs LinkedList: ┌──────────┬──────────────┬──────────────┐ │ 操作 │ ArrayList │ LinkedList │ ├──────────┼──────────────┼──────────────┤ │ 随机访问 │ O(1) 快 │ O(n) 慢 │ │ 插入删除 │ O(n) 慢 │ O(1) 快 │ │ 内存占用 │ 连续内存 │ 额外指针开销 │ │ 底层实现 │ 动态数组 │ 双向链表 │ └──────────┴──────────────┴──────────────┘
// ArrayList:基于数组 List arrayList = new ArrayList<>(); arrayList.get(0); // O(1) 快速访问 arrayList.add(0, "item"); // O(n) 需要移动元素 // LinkedList:基于链表 List linkedList = new LinkedList<>(); linkedList.get(0); // O(n) 需要遍历 linkedList.addFirst("item"); // O(1) 快速插入

使用场景:

  • ArrayList:频繁随机访问,少量插入删除
  • LinkedList:频繁插入删除,少量随机访问
💡 实际开发:90% 的场景用 ArrayList 就够了,因为现代 CPU 缓存对连续内存友好。
3. HashMap 的底层实现原理?JDK 1.8 有什么优化? 困难

底层结构:数组 + 链表 + 红黑树(JDK 1.8+)

HashMap 结构(JDK 1.8): 数组(桶) ┌─────┬─────┬─────┬─────┐ │ 0 │ 1 │ 2 │ 3 │ └──┬──┴─────┴──┬──┴─────┘ │ │ ↓ ↓ 链表 红黑树 Node TreeNode ↓ ↓ Node TreeNode ↓ ↓ Node TreeNode 链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树 红黑树节点 ≤ 6 → 转回链表

JDK 1.8 优化:

  • 链表转红黑树:当链表长度 ≥ 8 时,转为红黑树,查询从 O(n) 优化到 O(log n)
  • 扩容优化:rehash 时,元素要么在原位置,要么在"原位置+旧容量",不需要重新计算 hash
  • 尾插法:JDK 1.7 用头插法,多线程下可能死循环;1.8 改为尾插法
// put 流程 public V put(K key, V value) { // 1. 计算 hash 值 int hash = hash(key); // 2. 定位桶位置 int index = (n - 1) & hash; // 3. 如果桶为空,直接插入 if (table[index] == null) { table[index] = new Node(key, value); } // 4. 如果桶不为空,处理冲突 else { // 链表或红黑树插入 } // 5. 检查是否需要扩容 if (++size > threshold) { resize(); } }
⚠️ 注意:HashMap 不是线程安全的,多线程环境下应该用 ConcurrentHashMap。
4. HashMap 的扩容机制是怎样的? 中等

扩容时机:当元素数量超过 容量 × 负载因子 时触发扩容。

  • 默认初始容量:16
  • 默认负载因子:0.75
  • 扩容倍数:2 倍
// 扩容示例 HashMap map = new HashMap<>(); // 初始容量 16,阈值 16 * 0.75 = 12 // 当添加第 13 个元素时,触发扩容到 32 // 指定初始容量(推荐) HashMap map = new HashMap<>(100); // 实际容量会是大于等于 100 的最小 2 的幂次,即 128

扩容过程:

  1. 创建一个新数组,容量是原来的 2 倍
  2. 遍历旧数组,重新计算每个元素的位置
  3. 将元素移动到新数组
💡 性能优化:如果知道大概的元素数量,建议在创建时指定初始容量,避免多次扩容。
5. ConcurrentHashMap 的实现原理?JDK 1.8 有什么变化? 困难

JDK 1.7:分段锁(Segment)

JDK 1.7 结构: Segment[] segments ├─ Segment 0(继承 ReentrantLock) │ └─ HashEntry[] ├─ Segment 1 │ └─ HashEntry[] └─ Segment 2 └─ HashEntry[] 默认 16 个 Segment,最多支持 16 个线程并发

JDK 1.8:CAS + synchronized

JDK 1.8 结构: Node[] table(类似 HashMap) ├─ 桶为空:CAS 插入 ├─ 桶不为空:synchronized 锁住桶头节点 └─ 链表或红黑树 并发度更高,锁粒度更细
// JDK 1.8 put 流程 public V put(K key, V value) { for (Node[] tab = table;;) { // 1. 如果桶为空,CAS 插入 if (tabAt(tab, i) == null) { if (casTabAt(tab, i, null, new Node(key, value))) break; } // 2. 如果桶不为空,synchronized 锁住桶头 else { synchronized (f) { // 链表或红黑树插入 } } } }

优势:

  • 锁粒度更细,并发度更高
  • 减少内存占用(不需要 Segment)
  • 查询性能更好(不需要两次 hash)
6. HashSet 的底层实现是什么? 简单

底层实现:HashSet 内部使用 HashMap 实现。

public class HashSet { private transient HashMap map; private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT) == null; } public boolean contains(Object o) { return map.containsKey(o); } }
  • 元素存储在 HashMap 的 key 中
  • value 统一使用一个固定的 Object 对象
  • 利用 HashMap 的 key 不重复特性实现 Set
💡 同理:LinkedHashSet 基于 LinkedHashMap,TreeSet 基于 TreeMap。
7. HashMap 和 Hashtable 的区别? 中等
HashMap vs Hashtable: ┌──────────┬──────────────┬──────────────┐ │ 特性 │ HashMap │ Hashtable │ ├──────────┼──────────────┼──────────────┤ │ 线程安全 │ 否 │ 是 │ │ null key │ 允许1个 │ 不允许 │ │ null value│ 允许多个 │ 不允许 │ │ 性能 │ 快 │ 慢(同步) │ │ 继承 │ AbstractMap │ Dictionary │ │ 推荐度 │ 推荐 │ 已过时 │ └──────────┴──────────────┴──────────────┘
// HashMap:允许 null Map hashMap = new HashMap<>(); hashMap.put(null, "value"); // OK hashMap.put("key", null); // OK // Hashtable:不允许 null Map hashtable = new Hashtable<>(); hashtable.put(null, "value"); // NullPointerException hashtable.put("key", null); // NullPointerException
⚠️ 注意:Hashtable 已过时,不推荐使用。需要线程安全应该用 ConcurrentHashMap。
8. 如何解决 HashMap 的线程安全问题? 中等

三种方案:

// 方案1:使用 ConcurrentHashMap(推荐) Map map = new ConcurrentHashMap<>(); // 方案2:使用 Collections.synchronizedMap Map map = Collections.synchronizedMap(new HashMap<>()); // 方案3:使用 Hashtable(不推荐,已过时) Map map = new Hashtable<>();

性能对比:

  • ConcurrentHashMap:分段锁/CAS,并发度高,性能最好
  • synchronizedMap:全局锁,并发度低,性能一般
  • Hashtable:全局锁,性能最差,已过时
💡 推荐:多线程环境下,首选 ConcurrentHashMap。
9. ArrayList 的扩容机制是怎样的? 简单

扩容规则:每次扩容为原容量的 1.5 倍。

// ArrayList 扩容源码 private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍 if (newCapacity < minCapacity) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } // 使用示例 List list = new ArrayList<>(); // 初始容量 0 list.add("a"); // 扩容到 10 // 添加到第 11 个元素时,扩容到 15 // 添加到第 16 个元素时,扩容到 22
💡 性能优化:如果知道大概的元素数量,建议在创建时指定初始容量:new ArrayList<>(100)
10. 如何实现一个线程安全的 List? 中等

四种方案:

// 方案1:Vector(不推荐,已过时) List list = new Vector<>(); // 方案2:Collections.synchronizedList List list = Collections.synchronizedList(new ArrayList<>()); // 方案3:CopyOnWriteArrayList(推荐,读多写少) List list = new CopyOnWriteArrayList<>(); // 方案4:手动加锁 List list = new ArrayList<>(); synchronized (list) { list.add("item"); }

CopyOnWriteArrayList 原理:

  • 写操作时,复制一份新数组,在新数组上修改,然后替换旧数组
  • 读操作不加锁,直接读取
  • 适合读多写少的场景
⚠️ 注意:CopyOnWriteArrayList 写操作开销大,不适合写多的场景。
11. HashMap 为什么用红黑树而不是 AVL 树? 困难

红黑树 vs AVL 树:

特性对比: ┌──────────┬──────────┬──────────┐ │ 特性 │ 红黑树 │ AVL 树 │ ├──────────┼──────────┼──────────┤ │ 平衡条件 │ 宽松 │ 严格 │ │ 查询性能 │ O(log n) │ O(log n) │ │ 插入性能 │ 较快 │ 较慢 │ │ 删除性能 │ 较快 │ 较慢 │ │ 旋转次数 │ 最多3次 │ 可能多次 │ └──────────┴──────────┴──────────┘

选择红黑树的原因:

  • 插入删除更快:红黑树的平衡条件更宽松,插入删除时旋转次数更少
  • 查询性能差距小:虽然 AVL 树更平衡,但查询性能差距不大
  • 综合性能更好:HashMap 需要频繁插入删除,红黑树更适合
💡 实际应用:红黑树广泛应用于 Java(HashMap、TreeMap)、Linux 内核、C++ STL 等。
12. 如何遍历 Map?哪种方式性能最好? 中等
Map map = new HashMap<>(); // 方式1:entrySet(推荐,性能最好) for (Map.Entry entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); } // 方式2:keySet(需要 key 和 value 时不推荐) for (String key : map.keySet()) { Integer value = map.get(key); // 额外的查询开销 } // 方式3:values(只需要 value 时使用) for (Integer value : map.values()) { // 只能获取 value } // 方式4:forEach(JDK 8+,简洁) map.forEach((key, value) -> { System.out.println(key + ": " + value); }); // 方式5:Stream API(JDK 8+,功能强大) map.entrySet().stream() .filter(e -> e.getValue() > 10) .forEach(e -> System.out.println(e.getKey()));

性能对比:

  • entrySet:最快,只遍历一次
  • keySet:较慢,需要额外的 get 操作
  • forEach:与 entrySet 性能相当,代码更简洁
💡 推荐:需要 key 和 value 时用 entrySet 或 forEach,只需要 key 时用 keySet,只需要 value 时用 values。
13. fail-fast 和 fail-safe 的区别? 简单

fail-fast(快速失败):遍历时检测到修改,立即抛出 ConcurrentModificationException。

// fail-fast 示例 List list = new ArrayList<>(Arrays.asList("a", "b", "c")); for (String item : list) { list.remove(item); // ConcurrentModificationException }

fail-safe(安全失败):遍历时操作副本,不会抛异常,但可能看不到最新数据。

// fail-safe 示例 List list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c")); for (String item : list) { list.remove(item); // 不会抛异常 }
  • fail-fast:ArrayList、HashMap 等
  • fail-safe:CopyOnWriteArrayList、ConcurrentHashMap 等
14. 如何正确删除 List 中的元素? 中等
List list = new ArrayList<>(Arrays.asList("a", "b", "c", "d")); // 错误方式1:for-each 中删除 for (String item : list) { if (item.equals("b")) { list.remove(item); // ConcurrentModificationException } } // 错误方式2:普通 for 循环删除(会漏删) for (int i = 0; i < list.size(); i++) { if (list.get(i).equals("b")) { list.remove(i); // 删除后,后面的元素前移,导致漏删 } } // 正确方式1:Iterator.remove() Iterator it = list.iterator(); while (it.hasNext()) { String item = it.next(); if (item.equals("b")) { it.remove(); // 使用迭代器的 remove } } // 正确方式2:倒序遍历 for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i).equals("b")) { list.remove(i); } } // 正确方式3:removeIf(JDK 8+,推荐) list.removeIf(item -> item.equals("b")); // 正确方式4:Stream filter(JDK 8+) list = list.stream() .filter(item -> !item.equals("b")) .collect(Collectors.toList());
💡 推荐:JDK 8+ 优先使用 removeIf 或 Stream,代码更简洁安全。
15. Queue 和 Deque 的区别?有哪些常用实现类? 中等
Queue vs Deque 对比: ┌──────────┬──────────────┬──────────────┐ │ 特性 │ Queue │ Deque │ ├──────────┼──────────────┼──────────────┤ │ 全称 │ 队列 │ 双端队列 │ │ 操作 │ FIFO │ 两端都可操作 │ │ 主要方法 │ offer/poll │ offerFirst/ │ │ │ peek/element │ offerLast │ │ │ │ pollFirst/ │ │ │ │ pollLast │ │ 典型场景 │ 任务队列 │ 栈/队列/双端 │ └──────────┴──────────────┴──────────────┘ 常用实现类: Queue:LinkedList、PriorityQueue、ArrayBlockingQueue Deque:LinkedList、ArrayDeque、LinkedBlockingDeque
// Queue 基本操作(FIFO) Queue queue = new LinkedList<>(); queue.offer("A"); // 入队,返回 boolean queue.offer("B"); String first = queue.peek(); // 查看队首,不删除 String polled = queue.poll(); // 出队,删除并返回 // Deque 双端操作 Deque deque = new ArrayDeque<>(); deque.offerFirst("First"); // 头部插入 deque.offerLast("Last"); // 尾部插入 String first = deque.peekFirst(); // 查看头部 String last = deque.peekLast(); // 查看尾部 deque.pollFirst(); // 头部删除 deque.pollLast(); // 尾部删除 // Deque 可以当栈使用(LIFO) Deque stack = new ArrayDeque<>(); stack.push("A"); // 等价于 addFirst stack.push("B"); String top = stack.pop(); // 等价于 removeFirst
💡 实战建议:ArrayDeque 是栈和队列的最佳实现,性能优于 LinkedList 和 Stack。
16. PriorityQueue 的原理是什么?如何自定义排序? 中等

核心原理:PriorityQueue 基于优先堆(默认小顶堆)实现,保证队首元素始终是最小(或最大)的。

PriorityQueue 内部结构: 1(最小) / \ 3 5 / \ / \ 9 12 8 16 特性: • 底层:动态数组 + 堆排序 • 时间复杂度:O(log n) 插入/删除,O(1) 查看最值 • 默认:自然排序(小顶堆) • 线程安全:不安全,需用 PriorityBlockingQueue
// 默认小顶堆 PriorityQueue minHeap = new PriorityQueue<>(); minHeap.offer(5); minHeap.offer(2); minHeap.offer(8); System.out.println(minHeap.poll()); // 2(最小值) // 大顶堆:自定义比较器 PriorityQueue maxHeap = new PriorityQueue<>((a, b) -> b - a); maxHeap.offer(5); maxHeap.offer(2); maxHeap.offer(8); System.out.println(maxHeap.poll()); // 8(最大值) // 对象自定义排序 PriorityQueue taskQueue = new PriorityQueue<>(Comparator.comparing(Task::getPriority)); class Task { private String name; private int priority; // 数字越小优先级越高 public int getPriority() { return priority; } // getter/setter... }
⚠️ 注意:PriorityQueue 的 iterator() 不保证有序,只有 poll() 才能按优先级获取元素。
17. Properties 和 HashMap 的区别?什么时候用 Properties? 中等
Properties vs HashMap 对比: ┌──────────┬──────────────┬──────────────┐ │ 特性 │ Properties │ HashMap │ ├──────────┼──────────────┼──────────────┤ │ 继承关系 │ Hashtable │ AbstractMap │ │ 键值类型 │ 仅 String │ 任意泛型 │ │ 线程安全 │ 安全(同步) │ 不安全 │ │ 特殊功能 │ load/store │ 无 │ │ 典型用途 │ 配置文件 │ 通用映射 │ │ 性能 │ 较慢 │ 较快 │ └──────────┴──────────────┴──────────────┘
// Properties 基本用法 Properties props = new Properties(); props.setProperty("db.url", "jdbc:mysql://localhost:3306/test"); props.setProperty("db.username", "root"); props.setProperty("db.password", "123456"); // 从配置文件加载 try (InputStream input = new FileInputStream("config.properties")) { props.load(input); } // 保存到配置文件 try (OutputStream output = new FileOutputStream("config.properties")) { props.store(output, "Database Configuration"); } // 获取配置(有默认值) String url = props.getProperty("db.url", "default_url"); String timeout = props.getProperty("connection.timeout", "5000"); // 遍历所有配置 props.forEach((key, value) -> System.out.println(key + " = " + value));
💡 使用场景:Properties 专门用于处理配置文件,支持 .properties 和 .xml 格式,是配置管理的首选。
18. Collections 工具类有哪些常用方法? 中等
Collections 工具类分类: ┌─────────────────┬─────────────────────────────┐ │ 分类 │ 主要方法 │ ├─────────────────┼─────────────────────────────┤ │ 排序操作 │ sort、reverse、shuffle、swap │ │ 查找操作 │ binarySearch、max、min │ │ 不可变集合 │ unmodifiableXxx │ │ 同步集合 │ synchronizedXxx │ │ 空集合 │ emptyList、emptySet、emptyMap│ │ 单元素集合 │ singletonList、singletonSet │ │ 频率统计 │ frequency、disjoint │ │ 填充替换 │ fill、replaceAll、rotate │ └─────────────────┴─────────────────────────────┘
// 1. 排序操作 List list = Arrays.asList(3, 1, 4, 1, 5); Collections.sort(list); // 升序排序 Collections.reverse(list); // 反转 Collections.shuffle(list); // 随机打乱 Collections.swap(list, 0, 4); // 交换位置 // 2. 查找操作(需要先排序) Collections.sort(list); int index = Collections.binarySearch(list, 4); // 二分查找 Integer max = Collections.max(list); // 最大值 Integer min = Collections.min(list); // 最小值 // 3. 创建特殊集合 List emptyList = Collections.emptyList(); // 不可变空列表 List singletonList = Collections.singletonList("only"); // 单元素列表 List unmodifiableList = Collections.unmodifiableList(new ArrayList<>()); // 不可变列表 // 4. 同步集合(线程安全) List syncList = Collections.synchronizedList(new ArrayList<>()); Map syncMap = Collections.synchronizedMap(new HashMap<>()); // 5. 频率统计 List words = Arrays.asList("a", "b", "a", "c", "b", "a"); int count = Collections.frequency(words, "a"); // 3 boolean disjoint = Collections.disjoint(list1, list2); // 是否无交集 // 6. 填充和替换 List numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); Collections.fill(numbers, 0); // 全部填充为0 Collections.replaceAll(numbers, 0, 1); // 替换所有0为1 Collections.rotate(numbers, 2); // 向右旋转2位
⚠️ 注意:同步集合的性能较差,并发场景优先使用 ConcurrentHashMap 等并发集合。
19. 【阿里巴巴】如何设计一个线程安全的集合?请分析 CopyOnWriteArrayList 的原理和适用场景。 困难

核心考察点:线程安全集合设计、CopyOnWrite 机制、读写分离思想、并发编程实践。

完美答案

从设计思想到具体实现的完整分析:

线程安全集合设计策略: ┌─────────────────┬──────────────┬──────────────┐ │ 策略 │ 实现方式 │ 典型代表 │ ├─────────────────┼──────────────┼──────────────┤ │ 同步(synchronized)│ 方法加锁 │ Vector、Hashtable │ │ 细粒度锁 │ 分段锁、CAS │ ConcurrentHashMap │ │ 读写分离 │ CopyOnWrite │ CopyOnWriteArrayList │ │ 不变对象 │ 不可变设计 │ ImmutableCollections │ └─────────────────┴──────────────┴──────────────┘
// CopyOnWriteArrayList 核心原理 public class CopyOnWriteArrayList { private volatile Object[] array; public E get(int index) { return (E) array[index]; // 无锁读取 } public boolean add(E e) { lock.lock(); try { Object[] newArr = Arrays.copyOf(array, array.length + 1); newArr[array.length] = e; array = newArr; // 原子替换 return true; } finally { lock.unlock(); } } }

CopyOnWriteArrayList 特性分析:

  • 读写分离:读操作无锁,写操作复制新数组
  • 最终一致性:写操作完成后,新读操作才能看到新数据
  • 内存开销:每次写操作都需要复制整个数组
  • 迭代安全:迭代器基于快照,不会抛出 ConcurrentModificationException
// 适用场景:事件监听器管理(读多写少) List listeners = new CopyOnWriteArrayList<>(); listeners.add(listener); // 偶尔写 for (EventListener l : listeners) l.onEvent(event); // 频繁读

适用场景:

  • 读多写少:读操作远多于写操作的场景
  • 数据量不大:集合大小相对可控
  • 迭代频繁:需要频繁遍历且不允许迭代时修改
  • 事件监听:监听器管理、配置管理等场景

不适用场景:

  • 写操作频繁:会导致大量数组复制,性能差
  • 数据量大:内存开销和复制成本过高
  • 强一致性:需要实时看到最新数据的场景
💡 阿里面试加分点:能够对比 CopyOnWriteArrayList 与 Collections.synchronizedList、ConcurrentLinkedQueue 的差异,并根据具体场景选择合适的并发集合。
20. 【字节跳动】如何实现一个 LRU 缓存?请分析 LinkedHashMap 的源码实现。 困难

核心考察点:LRU 算法、LinkedHashMap 原理、数据结构设计、缓存实现思路。

完美答案

从算法原理到源码分析的完整实现:

LRU 缓存设计思路: ┌─────────────────┬─────────────────────────────┐ │ 核心思想 │ 最近最少使用的数据优先淘汰 │ ├─────────────────┼─────────────────────────────┤ │ 数据结构 │ 哈希表 + 双向链表 │ ├─────────────────┼─────────────────────────────┤ │ 时间复杂度 │ O(1) 查找、插入、删除 │ ├─────────────────┼─────────────────────────────┤ │ Java 实现 │ LinkedHashMap(accessOrder) │ └─────────────────┴─────────────────────────────┘
// 方式1:基于 LinkedHashMap 实现 LRU 缓存 public class LRUCache extends LinkedHashMap { private final int maxCapacity; public LRUCache(int initialCapacity, int maxCapacity) { super(initialCapacity, 0.75f, true); // accessOrder=true this.maxCapacity = maxCapacity; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > maxCapacity; // 超过容量时删除最老元素 } // 使用示例 public static void main(String[] args) { LRUCache cache = new LRUCache<>(16, 3); cache.put("A", 1); cache.put("B", 2); cache.put("C", 3); System.out.println(cache); // {A=1, B=2, C=3} cache.get("A"); // 访问 A,A 变为最新 cache.put("D", 4); // 添加 D,最老的 B 被淘汰 System.out.println(cache); // {C=3, A=1, D=4} } }
// 方式2:手写LRU(HashMap + 双向链表) class LRUCache { private Map cache = new HashMap<>(); private Node head, tail; private int capacity; public V get(K key) { Node n = cache.get(key); if (n == null) return null; moveToHead(n); return n.value; } public void put(K key, V value) { Node n = cache.get(key); if (n != null) { n.value = value; moveToHead(n); } else { n = new Node(key, value); cache.put(key, n); addToHead(n); if (cache.size() > capacity) { cache.remove(tail.prev.key); removeNode(tail.prev); } } } }
💡 字节面试加分点:能够分析 LinkedHashMap 与 HashMap 的性能差异,理解双向链表维护的时间复杂度,并能手写完整的 LRU 缓存实现。

🏆 大厂面试真题专区

21. 【阿里巴巴】手写一个线程安全的 HashMap,如何解决哈希冲突和扩容问题? 困难

核心考察点:哈希表实现、线程安全设计、冲突解决、扩容机制、并发优化。

完美答案

从基础实现到并发优化的完整方案:

// 线程安全HashMap核心实现 class ThreadSafeHashMap { private Node[] table; private int size; public synchronized V put(K key, V value) { if (size >= table.length * 0.75) resize(); int i = hash(key) % table.length; for (Node n = table[i]; n != null; n = n.next) { if (n.key.equals(key)) { n.value = value; return n.value; } } table[i] = new Node<>(key, value, table[i]); size++; return null; } public synchronized V get(K key) { int i = hash(key) % table.length; for (Node n = table[i]; n != null; n = n.next) { if (n.key.equals(key)) return n.value; } return null; } } // 分段锁优化(类似JDK7 ConcurrentHashMap) class SegmentedHashMap { private Segment[] segments = new Segment[16]; public V put(K key, V value) { return segments[hash(key) % 16].put(key, value); } }

关键技术要点:

  • 哈希冲突解决:链地址法,当冲突时使用链表存储
  • 扩容机制:当 size > capacity * loadFactor 时触发扩容
  • 线程安全:使用 synchronized 或分段锁保证并发安全
  • 性能优化:分段锁减少锁竞争,提高并发性能
💡 阿里面试加分点:能够对比不同实现方式的优缺点,理解 ConcurrentHashMap 的设计思想,并能分析扩容时的性能问题。
22. 【字节跳动】设计一个支持时间窗口的限流器,要求精确控制 QPS 困难

核心考察点:限流算法设计、时间窗口控制、高并发处理、精确度优化。

完美答案

从基础限流到高精度时间窗口的完整实现:

// 滑动窗口限流器 class SlidingWindowRateLimiter { private Queue requests = new LinkedList<>(); private int maxRequests; private long windowMillis; public synchronized boolean allow() { long now = System.currentTimeMillis(); while (!requests.isEmpty() && now - requests.peek() > windowMillis) { requests.poll(); } if (requests.size() < maxRequests) { requests.offer(now); return true; } return false; } } // 分布式限流(Redis + Lua) String lua = "redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1]) " + "if redis.call('zcard', KEYS[1]) < tonumber(ARGV[2]) then " + " redis.call('zadd', KEYS[1], ARGV[3], ARGV[4]); return 1 " + "else return 0 end"; jedis.eval(lua, 1, key, windowStart, maxReq, now, clientId);

高精度限流关键技术:

  • 时间精度:使用环形数组将时间窗口细分,提高精度
  • 内存优化:固定大小数组,避免内存泄漏
  • 原子性:使用 Lua 脚本保证分布式环境下的原子操作
  • 性能考虑:减少锁竞争,使用无锁数据结构
💡 字节面试加分点:能够分析不同限流算法的精度和性能差异,理解分布式环境下的一致性问题,并能设计出高精度的限流器。
23. 【腾讯】手写一个内存缓存,支持 LRU 淘汰和过期时间 困难

核心考察点:缓存设计、LRU 算法、过期机制、并发安全、性能优化。

完美答案

从基础缓存到功能完善的内存缓存系统:

// 内存缓存:LRU + 过期时间 class MemoryCache { class Node { K key; V value; long expireTime; Node prev, next; } private Map cache = new HashMap<>(); private Node head, tail; private int maxSize; public synchronized void put(K key, V value, long ttlMs) { Node n = cache.get(key); if (n != null) { n.value = value; n.expireTime = System.currentTimeMillis() + ttlMs; moveToHead(n); } else { n = new Node(key, value, System.currentTimeMillis() + ttlMs); if (cache.size() >= maxSize) removeTail(); cache.put(key, n); addToHead(n); } } public synchronized V get(K key) { Node n = cache.get(key); if (n == null) return null; if (System.currentTimeMillis() > n.expireTime) { removeNode(n); cache.remove(key); return null; } moveToHead(n); return n.value; } }

内存缓存设计要点:

  • LRU 淘汰:使用双向链表维护访问顺序
  • 过期机制:记录过期时间,定期清理过期条目
  • 并发安全:使用 ConcurrentHashMap 和读写锁提高并发性能
  • 内存管理:限制最大容量,避免内存溢出
  • 性能优化:减少锁竞争,使用 volatile 保证可见性
💡 腾讯面试加分点:能够分析不同缓存实现方式的性能差异,理解并发安全的设计要点,并能设计出高性能的内存缓存系统。