前言
集合是 Java 开发每日必用的数据容器,不管是接口接收参数、业务数据存储、结果去重、键值映射都离不开 List、Set、Map。 但绝大多数开发者只会调用add()、get()、put(),对底层存储结构、扩容逻辑、哈希冲突、线程安全一知半解,线上频繁出现内存溢出、查询缓慢、数据重复、并发丢数据等问题。 本文搭配体系结构图、对比表格、底层源码拆解,一次性吃透 Java 集合全体系,同时覆盖面试高频考点,看完既能应对八股面试,也能规范业务开发选型。
一、集合框架完整体系梳理
Java 集合分为两大根接口:Collection单列集合、Map双列键值集合。
- Collection(单列,存单个元素)
- List:有序、可重复、支持索引;实现类:ArrayList、LinkedList、Vector
- Set:无序、不可重复;实现类:HashSet、TreeSet、LinkedHashSet
- Queue:队列,先进先出;实现类:ArrayDeque、PriorityQueue
- Map(双列,键值对 key-value)
- HashMap:无序键值存储,哈希表实现
- TreeMap:按键排序
- LinkedHashMap:插入有序 HashMap
- HashTable:老旧线程安全 Map(不推荐使用)
上图完整展示接口继承、实现类层级关系,区分 List/Set/Map 三大分支,是学习集合的基础,面试常让手绘该结构图。
二、ArrayList vs LinkedList 底层结构与业务选型
2.1 底层存储源码对比
ArrayList
底层是动态 Object 数组,默认初始化容量 10,存连续内存:
java
运行
transient Object[] elementData; // 存储元素数组 private int size; // 当前元素数量 // 扩容机制:扩容至原容量1.5倍 private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); }扩容流程:新增元素超出数组长度 → 创建 1.5 倍长度新数组 →System.arraycopy拷贝全部元素,频繁扩容会产生大量数组拷贝损耗性能。
LinkedList
底层是双向链表,无数组、无扩容概念,每个节点存前驱、后继指针:
java
运行
private static class Node<E> { E item; Node<E> next; // 下一个节点 Node<E> prev; // 上一个节点 }元素分散在堆内存,无连续地址,依靠指针串联。
2.2 核心性能对比表
表格
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机 get 查询 | O (1),直接数组下标寻址 | O (n),需从头遍历节点 |
| 头部插入删除 | O (n),数组全部元素后移 | O (1),仅修改前后节点指针 |
| 尾部追加 | O (1)(不触发扩容) | O(1) |
| 内存占用 | 紧凑连续内存,开销小 | 每个节点额外存两个指针,内存开销大 |
| 遍历效率 | for 循环、Iterator 效率极高 | 不推荐普通 for 循环,只能用迭代器 |
2.3 业务选型规范
- 90% 日常业务优先
ArrayList:绝大多数场景都是尾部新增、根据索引查询; - 仅高频头部 / 中间插入删除、数据量超大时选用
LinkedList; - 固定长度数据可使用
Arrays.asList或手动初始化容量,减少 ArrayList 扩容次数。
三、HashSet 去重原理与 equals/hashCode 强制约定
3.1 HashSet 底层本质
HashSet底层完全依赖HashMap实现,存入元素作为 HashMap 的 key,value 统一为静态空 Object:
java
运行
public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT)==null; }所以 HashSet 去重逻辑完全复用 HashMap 的 key 判重规则。
3.2 去重判断两步流程
- 调用对象
hashCode()计算哈希值,定位哈希桶; - 若桶内无元素 → 直接存入;若桶内已有元素,调用
equals()对比内容:- equals 返回 true:判定为重复,不存入;
- equals 返回 false:哈希冲突,链表 / 红黑树追加节点。
3.3 开发强制规范(面试必考题)
自定义实体存入 HashSet,必须同时重写hashCode()与equals(),缺一不可:
- 只重写 equals、不重写 hashCode:相同内容对象哈希值不同,判定为两个对象,无法去重;
- 只重写 hashCode、不重写 equals:不同内容可能哈希值碰撞,错误判定重复;
错误示例:
java
运行
class User { String id; // 仅重写equals,未重写hashCode @Override public boolean equals(Object o) { if (this == o) return true; User user = (User) o; return Objects.equals(id, user.id); } } // 两个id相同的User对象,hashCode不同,HashSet会同时存储,去重失效四、HashMap 工作原理(全文核心重点)
4.1 底层存储结构(JDK1.8 优化)
- 主体:
Node<K,V>[] table哈希数组(哈希桶); - 哈希冲突少时,桶内元素用单向链表存储;
- 链表长度≥8 且 数组容量≥64,链表转为红黑树,查询时间复杂度从 O (n) 降至 O (logn);
- 红黑树节点数量≤6,退化为链表。
4.2 put 插入完整流程
- 计算 key 哈希值
hash = (key.hashCode()) ^ (hash >>> 16),高低位混合减少冲突; - 通过
hash & (table.length - 1)计算数组下标; - 判断哈希桶是否为空,为空直接新建 Node 放入;
- 桶不为空,对比桶首节点 key:hash 相等 && (== 或 equals) → 覆盖旧 value;
- key 不匹配,判断当前桶是红黑树还是链表:
- 红黑树:树内查找 key,存在则覆盖,不存在新增树节点;
- 链表:循环遍历链表,找到相同 key 覆盖,末尾追加新节点;追加后链表长度达到 8,触发树化;
- 插入完成,size 自增,若
size > 阈值(容量*0.75),触发 2 倍扩容。
4.3 关键参数详解
- 默认初始容量:16,容量永远是 2 的幂次方,方便位运算取模;
- 负载因子 0.75:达到容量 75% 时扩容,平衡哈希冲突与内存占用;
- 树化阈值 8、退化阈值 6;
- 扩容规则:容量翻倍,重新计算所有元素哈希下标,元素分为高低两条链表迁移。
4.4 开发避坑点
- 预估数据量时,初始化 HashMap 指定容量:
new HashMap<>(预估数量/0.75 + 1),避免多次扩容; - 自定义对象作为 key,必须重写 hashCode 与 equals;
- HashMap 非线程安全,并发场景使用
ConcurrentHashMap,禁止手动加锁 HashMap。
五、集合遍历的 5 种方式、优缺点与使用场景
5.1 普通 for 循环(仅 List 可用)
java
运行
ArrayList<String> list = new ArrayList<>(); for(int i = 0; i < list.size(); i++){ String s = list.get(i); }优点:可获取索引、支持遍历中修改元素; 缺点:LinkedList 使用该方式效率极低,每次 get 都从头遍历。
5.2 增强 for 循环(foreach,全部集合通用)
java
运行
for(String s : list){ System.out.println(s); }底层依赖迭代器 Iterator,语法简洁; 坑:遍历中调用集合add/remove会触发ConcurrentModificationException并发修改异常。
5.3 Iterator 迭代器(通用,支持安全删除)
java
运行
Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); it.remove(); // 迭代器自身删除方法,不会报并发异常 }适用:遍历过程中需要删除元素的场景。
5.4 ListIterator(仅 List 独有,可前后遍历)
java
运行
ListIterator<String> lit = list.listIterator(); while(lit.hasNext()){} while(lit.hasPrevious()){} // 反向遍历适合需要双向遍历 List 的特殊业务。
5.5 Java8 Stream 流式遍历
java
运行
list.stream().forEach(System.out::println); // 并行流大数据处理 list.parallelStream().filter(s->s.length()>1).collect(Collectors.toList());优点:支持过滤、映射、分组、并行处理,函数式编程简化代码; 缺点:复杂逻辑可读性下降,并行流注意线程安全问题。
遍历选型总结
- ArrayList 需要索引操作:普通 for 循环;
- 简单遍历、无需增删:foreach 增强 for;
- 遍历过程删除元素:Iterator 迭代器;
- 数据过滤、转换、分组、大数据并行:Stream 流;
- LinkedList 禁止使用普通 for 循环。
六、集合高频开发总结 & 面试考点
- List:随机查询选 ArrayList,频繁中间插入删除选 LinkedList;
- Set 去重核心依赖 hashCode+equals,自定义实体必须两个方法一起重写;
- HashMap 核心:数组 + 链表 + 红黑树、put 完整流程、2 倍扩容、负载因子 0.75,并发使用 ConcurrentHashMap;
- 遍历规避并发修改异常:遍历增删元素只用 Iterator.remove ();
- 所有集合非线程安全(Vector/HashTable 除外,性能差不推荐),多线程场景选用 JUC 并发容器。
结尾
Java 集合框架是后端面试第一高频模块,底层数组、链表、哈希、红黑树逻辑贯穿性能优化、线上故障排查。熟练掌握底层原理后,后续缓存、中间件底层哈希结构都能快速理解。