Java集合框架面试核心考点全解析
面试官考点分析:
- Java 集合框架的体系结构:考察 Collection、Map 及其常用子接口的继承关系与整体设计。
- 核心集合类的底层实现:如 ArrayList、LinkedList、HashMap、TreeMap 的数据结构与算法复杂度,扩容、树化机制。
- 线程安全与并发集合:Synchronized 包装、ConcurrentHashMap、CopyOnWriteArrayList 等并发工具的原理与适用场景。
- 应用场景与框架集成:在日常开发与 Spring、MyBatis、消息队列等主流技术中如何选择合适的集合类。
- JVM 与集合的交互:泛型擦除、迭代器 fail-fast 机制、内存占用与 GC 影响等容易被追问的深层知识点。
1. 标准回答
Java 集合类是java.util包下用于存储和管理对象的工具类,整体框架分为两大接口:Collection和Map。
- Collection 接口:单列集合的根接口,派生出List、Set、Queue三个子接口。
- List:有序、可重复。常用实现类:ArrayList(动态数组)、LinkedList(双向链表)、Vector(线程安全,已过时)。
- Set:无序、不可重复。常用实现类:HashSet(基于 HashMap)、TreeSet(红黑树)、LinkedHashSet(维护插入顺序)。
- Queue / Deque:队列/双端队列。常用:ArrayDeque、PriorityQueue、LinkedList(也实现 Deque)。
- Map 接口:双列集合,存储键值对。常用实现类:HashMap(数组+链表+红黑树)、TreeMap(红黑树)、LinkedHashMap(维护插入顺序/访问顺序)、Hashtable(线程安全,已过时)。并发场景下使用ConcurrentHashMap。
2. 核心原理
2.1 ArrayList vs LinkedList
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 Object[] | 双向链表 |
| 随机访问(get) | 快 O(1) | 慢 O(n) |
| 插入/删除(非尾部) | 慢 O(n) | 快 O(1)(定位到位置后) |
| 内存占用 | 较少(仅数组) | 较多(节点前驱后驱指针) |
| 线程安全 | 否 | 否 |
2.2 HashMap 底层原理
数据结构:JDK 7 为数组+链表;JDK 8 起当链表长度超过8且数组容量 ≥64时链表转为红黑树,查询时间复杂度从 O(n) 降为 O(log n)。
put 流程:
- 对 key 的 hashCode() 进行扰动处理(高 16 位与低 16 位异或)。
- 计算索引:(n - 1) & hash,找到数组位置。
- 若位置无元素,直接插入;若已有元素,判断 key 是否相同,相同则替换旧值;若为红黑树节点则插入树中;否则遍历链表插入尾部(JDK 8 尾插)并检查是否需要树化。
- 插入后若 size 超过阈值(容量 × 负载因子,默认 0.75),则进行扩容(容量变为原来的 2 倍),并重新哈希。
扩容机制:扩容时新建一个两倍长度的数组,重新计算每个元素的位置。JDK 8 利用 hash & oldCap 是否为 0 将链表直接拆分为两段,避免全部重新计算 hash,提高性能。
2.3 ConcurrentHashMap 原理
JDK 7 使用分段锁(Segment),每个 Segment 维护一个小的 HashMap,锁粒度较大。JDK 8 改用CAS + synchronized锁桶(Node),只有发生哈希碰撞且节点不为 null 时才加锁,并发度更高。同时还引入了红黑树,结构与 HashMap 类似。
2.4 HashSet / TreeSet / LinkedHashSet
HashSet底层就是一个HashMap,元素作为 key,value 为一个固定的 Object(PRESENT)。TreeSet基于TreeMap的红黑树实现,支持自然排序或自定义比较器。LinkedHashSet继承 HashSet,内部使用 LinkedHashMap 维护双向链表以保存插入顺序。
3. 应用场景
3.1 日常开发场景
- 数据列表展示:从数据库查出的多条记录一般用ArrayList存储,通过索引快速渲染或配合 Stream 流式处理。
- 去重/集合运算:两组数据取交集、并集、差集时使用HashSet去重,或利用 addAll、retainAll、removeAll 方法。
- 缓存队列:需要后进先出(LIFO)时用ArrayDeque模拟栈;需要先进先出(FIFO)时用LinkedList或ArrayDeque作为队列。
- 排序需求:需要自然排序或自定义排序的数据用TreeSet / TreeMap,或者用Collections.sort()对 List 排序。
- 键值查询:配置缓存、内存字典使用HashMap或LinkedHashMap(LRU 缓存)。
3.2 主流框架中的落地应用
- Spring 容器:Bean 定义的存储大量使用ConcurrentHashMap,如
DefaultListableBeanFactory中的beanDefinitionMap,保证并发注册的高效与安全。 - MyBatis:结果集映射时,默认返回ArrayList,当 resultType 为 Map 时返回HashMap或 LinkedHashMap。一级缓存和二级缓存内部也大量使用 Map。批量插入时常用 List 参数。
- 消息队列(如 RocketMQ):消费者端通过LinkedBlockingQueue或ArrayBlockingQueue缓存接收到的消息,实现流量削峰和异步处理。
- RPC 框架(如 Dubbo):服务实例缓存使用CopyOnWriteArrayList或ConcurrentHashMap,保证服务列表变更时的线程安全和高频读取性能。
- 日志框架(Log4j):异步日志输出器内部使用DisruptorRingBuffer(无锁队列)或ArrayBlockingQueue缓存日志事件。
- Tomcat 连接池:空闲连接管理使用ConcurrentLinkedDeque等并发队列,实现线程安全的连接复用。
4. 使用方式
4.1 ArrayList / LinkedList 基本操作
// ArrayList 基础用法 List<String> list = new ArrayList<>(); list.add("Hello"); list.add("World"); list.add(1, "Java"); // 在索引1处插入 String value = list.get(2); // 获取索引2的值 list.remove("World"); // 按对象删除 list.sort(String::compareTo); // 排序 System.out.println(list); // [Hello, Java] // LinkedList 作为队列使用 Deque<String> deque = new LinkedList<>(); deque.offer("A"); // 入队 deque.offer("B"); String head = deque.poll(); // 出队 "A"4.2 HashMap / LinkedHashMap / TreeMap
Map<String, Integer> map = new HashMap<>(); map.put("apple", 10); map.put("banana", 5); map.put("orange", 8); // 遍历方式1: entrySet for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); } // Java 8 Stream 操作 map.entrySet().stream() .sorted(Map.Entry.comparingByValue()) .forEach(e -> System.out.println(e.getKey())); // LinkedHashMap 实现 LRU LinkedHashMap<String, Integer> lruMap = new LinkedHashMap<String, Integer>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) { return size() > 100; // 超过100个元素删除最早访问的 } }; // TreeMap 自然排序 TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.putAll(map); // 自动按 key 字典序排序4.3 线程安全集合
// 1. Collections 工厂方法(全局加锁) List<String> syncList = Collections.synchronizedList(new ArrayList<>()); Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); // 2. 并发集合 Map<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("key", 1); // 线程安全的 List(写时复制,读多写少场景) List<String> cowList = new CopyOnWriteArrayList<>(); cowList.add("a");5. 扩展延伸
- WeakHashMap:键为弱引用,当键不再被其他强引用指向时,GC 会回收该键并自动从 map 中移除相应 entry。常用于需要缓存但又不希望阻止 GC 的场景。
- IdentityHashMap:使用==而非 equals() 比较 key,适合处理需要严格区分对象实例的场景(如序列化框架)。
- EnumSet / EnumMap:专为枚举类型设计的高效集合。内部用位向量实现,性能极高。
- BitSet:位集,适合存储大量布尔标志,比 boolean 数组节省大量空间,且支持位运算。
- Guava 的不可变集合:Google Guava 提供的
ImmutableList、ImmutableSet、ImmutableMap等,一旦创建不可修改,线程安全且节省内存。 - Java 9+ 静态工厂方法:
List.of()、Set.of()、Map.of()创建的集合也是不可变的,简化代码并提升安全性。
6. 面试追问
6.1 HashMap 为什么线程不安全?
JDK 7 扩容时采用头插法转移链表,多线程下可能产生循环链表,导致 get 时 CPU 100%。JDK 8 改为尾插法,避免了死循环,但多线程同时 put 仍可能出现数据覆盖、size 不准确等问题。原因是没有同步,所以必须使用ConcurrentHashMap或外部同步。
6.2 ConcurrentHashMap 的 key/value 为什么不能为 null?
官方解释:在并发环境中,无法明确判断一个 key 返回 null 是因为不存在,还是 value 就是 null。如果允许 null,调用containsKey()和get()之间状态可能已变,导致歧义。因此从设计上禁止 null。
6.3 ArrayList 扩容机制详解
默认初始容量为 10(JDK 8)。当添加元素超过底层数组容量时触发扩容,新容量 = 旧容量 + 旧容量 >> 1(即扩容 1.5 倍),并调用Arrays.copyOf()拷贝原有数据。若预知元素数量,应使用new ArrayList<>(initialCapacity)减少频繁扩容开销。
6.4 迭代器的 fail-fast 行为
在遍历集合时,如果使用集合自身的 add/remove 等方法修改结构(除迭代器自身的 remove),会抛出ConcurrentModificationException。因为集合维护一个 modCount,迭代器在 next() 时会检查该值是否改变。解法:使用迭代器的 remove 方法,或使用并发集合迭代器。
