写 Java 的人,基本绕不开集合。平时开发里,List、Set、Map 天天都在用,很多人也觉得自己已经挺熟了。可真到了面试,或者项目里碰到性能、并发、源码细节这些问题时,才发现自己对集合的理解其实并不扎实。
比如 ArrayList 和 LinkedList 到底该怎么选,HashMap 为什么查询快,HashSet 去重靠的是什么,subList() 为什么一不小心就埋坑。这些问题单看都不难,但一旦连起来问,很多人就容易乱。
这篇文章不准备照着教材把集合类挨个讲一遍,而是站在开发和面试两个角度,把 Java 集合里最常用、最容易问、也最容易踩坑的内容捋顺。你看完之后,至少能把选型思路、核心原理和高频问题弄明白。
1. Java 集合别死记,先抓住这两条主线
Java 集合框架看起来一大堆类,实际上先抓住两条主线就够了:
- Collection:单列集合,一个一个存元素
- Map:双列集合,以 key-value 方式存数据
而 Collection 下面,又可以继续拆成三类:
- List:有序,可重复
- Set:不可重复
- Queue:队列结构
用一张简单的结构图来看会更直观:
Collection ├─ List │ ├─ ArrayList │ ├─ LinkedList │ └─ Vector ├─ Set │ ├─ HashSet │ ├─ LinkedHashSet │ └─ TreeSet └─ Queue ├─ LinkedList └─ ArrayDeque Map ├─ HashMap ├─ LinkedHashMap ├─ Hashtable ├─ TreeMap └─ ConcurrentHashMap这张图不用全背,但你至少要意识到一件事:工程里最常打交道的集合,并没有那么多。
真正的主角,其实长期都是下面这几个:
- ArrayList
- HashSet
- HashMap
- TreeMap
- ConcurrentHashMap
把这几个吃透,集合这章基本就站住了。
2. List、Set、Map 到底怎么选,别再凭感觉了
很多初学者写代码时选集合,基本靠手感。想到列表就 List,想到键值对就 Map,剩下的能跑就行。短期看没问题,长期看会吃亏。
2.1 需要顺序、允许重复,用 List
只要你的数据需要“按顺序放着”,并且允许重复,优先考虑 List。
典型场景:
- 数据库查出来的一批用户
- 页面上的商品列表
- 按时间顺序记录的操作日志
而在 List 里,最常用的两个实现类是:
- ArrayList
- LinkedList
但这里有个很重要的结论:大多数业务场景里,默认优先 ArrayList。
2.2 需要去重,用 Set
如果你的核心需求不是“存一批数据”,而是“确保数据不重复”,那就该想到 Set。
比如:
- 一批手机号去重
- 用户标签去重
- 判断某个元素是否已经存在
常见实现类:
- HashSet:最常用,去重快
- LinkedHashSet:去重的同时保留插入顺序
- TreeSet:自动排序去重
2.3 需要映射关系,用 Map
只要数据天然是“一个 key 对应一个 value”,就优先用 Map。
典型场景:
- 用户 ID 对应用户对象
- 配置项名称对应配置值
- 单词对应出现次数
常见实现类:
- HashMap
- LinkedHashMap
- TreeMap
- ConcurrentHashMap
如果你现在只能记一条经验,那就记这一句:
默认列表选 ArrayList,默认映射选 HashMap,默认去重选 HashSet。
3. 为什么大家都在用 ArrayList,而不是 LinkedList
这是集合里最典型的“看上去会,实际上容易答偏”的问题。
很多人第一反应是:链表插入删除快,所以 LinkedList 应该更适合业务开发。这个说法只说对了一半。
3.1 ArrayList 的底层是动态数组
ArrayList 底层是动态数组,所以它的特点非常鲜明:
- 支持随机访问,get(index) 很快
- 尾部追加元素效率高
- 中间插入、删除需要移动元素
- 容量不够时会扩容
示例:
List<String> list = new ArrayList<>(); list.add("Java"); list.add("Spring"); list.add("MySQL"); System.out.println(list.get(1)); // Spring3.2 LinkedList 的底层是双向链表
LinkedList 的优势在于:
- 头尾插入删除方便
- 不需要像数组那样整体搬迁元素
但它的问题也很明显:
- 随机访问慢
- 查找某个位置时需要遍历
- CPU 缓存友好性通常不如数组
所以真实项目里,LinkedList 并没有很多人想象中那么常用。
更准确的理解应该是:
- 普通业务列表:优先 ArrayList
- 频繁头尾操作:再考虑 LinkedList 或 ArrayDeque
这也是为什么很多人写了几年 Java,项目里看到的 ArrayList 数量远远多于 LinkedList。
4. Set 为什么能去重,核心就在 equals 和 hashCode
集合里有一个点,面试一定会问,项目里也一定会碰到,那就是:Set 为什么能去重?
以 HashSet 为例,它底层其实是基于 HashMap 实现的。换句话说,HashSet 的去重能力,本质上来自 HashMap 的 key 不可重复。
当我们往 HashSet 里放对象时,通常会经历两个关键判断:
- 先比较 hashCode()
- 如果哈希值相同,再比较 equals()
所以一定要记住这个结论:
重写了 equals(),就必须同时重写 hashCode()。
来看一个例子:
class User { private String name; public User(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof User)) { return false; } User user = (User) o; return Objects.equals(name, user.name); } @Override public int hashCode() { return Objects.hash(name); } }如果你只重写 equals() 不重写 hashCode(),那么逻辑相等的两个对象,可能会因为哈希值不同而落到不同位置,最终导致 HashSet 去重失败。
这个坑,真不是只存在于面试题里。业务代码里自定义对象放入 Set、作为 Map 的 key 时,经常就会踩到。
5. HashMap 为什么这么重要,甚至可以说是集合的核心
如果说 Java 集合里有一个类是“你绕不过去的最终 boss”,那基本就是 HashMap。
5.1 HashMap 的底层结构,到底是什么
JDK 8 里的 HashMap,底层结构可以概括成一句话:
数组 + 链表 + 红黑树
它的工作过程大致是这样的:
- 先通过 hash 计算桶位置
- 如果桶里没有元素,直接放进去
- 如果有冲突,就先挂到链表上
- 如果链表太长,再转成红黑树
为什么要这么设计?
因为理想情况下,哈希定位能让查找接近 O(1);但一旦冲突多了,链表会让性能变差,于是 JDK 8 用红黑树来兜底。
5.2 HashMap 为什么查找快
说白了,HashMap 快,不是因为它“遍历得快”,而是因为它大多数时候根本不用全量遍历。
它先用 hash 把查找范围压缩到某个桶,再在这个桶里继续判断。只要哈希分布比较均匀,效率就会很高。
这也是为什么 HashMap 在业务开发中几乎随处可见。
5.3 关于 HashMap,你至少还要知道这几件事
1)它允许 null
HashMap:
- 允许 null key
- 允许 null value
但 ConcurrentHashMap 不允许,这是面试里的高频对比点。
2)已知数据量时,尽量初始化容量
如果你大概知道要放多少元素,创建时最好顺手指定容量,避免频繁扩容。
Map<String, Integer> map = new HashMap<>(16);在工程规范里,这也是一个很实用的优化习惯。
3)遍历时优先 entrySet
遍历 Map 时,优先用 entrySet(),不要总是先拿 keySet() 再去 get()。
for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); }6. TreeMap 和 TreeSet 不是冷门,它们只是有明确适用场景
很多人学集合时会把 TreeMap、TreeSet 当成“知道有这个东西就行”,其实没那么简单。
它们底层基于红黑树,最大的特点就是:
- 自动排序
- 支持有序遍历
- 适合范围查询
比如这些场景就很合适:
- 按分数排序
- 按日期维护数据
- 需要找“大于某个值的最小 key”
示例:
Map<Integer, String> treeMap = new TreeMap<>(); treeMap.put(3, "C"); treeMap.put(1, "A"); treeMap.put(2, "B"); System.out.println(treeMap); // {1=A, 2=B, 3=C}注意,TreeMap 和 TreeSet 要么依赖元素实现 Comparable,要么创建时显式传入 Comparator。
7. 并发环境下,别再默认用 HashMap 了
单线程环境用 HashMap 很正常,但只要你进入并发读写场景,就不能继续想当然了。
这时候更合适的选择通常是 ConcurrentHashMap。
Map<String, Integer> counterMap = new ConcurrentHashMap<>(); counterMap.put("Java", 1); counterMap.put("Spring", 2);它值得记住的点有三个:
- 线程安全
- 并发性能明显优于老的 Hashtable
- 不允许 null key 和 null value
很多老八股还在围着 Hashtable 讲线程安全,但真实开发里,优先考虑的基本都是 ConcurrentHashMap。
8. 这些集合坑,项目里真的太常见了
如果说源码考点更多是为面试准备,那下面这些坑就真的是为项目避雷准备的。
8.1 Arrays.asList() 不是普通可变 List
很多人会这样写:
List<String> list = Arrays.asList("A", "B", "C"); list.add("D");然后程序直接报错。
原因是 Arrays.asList() 返回的是固定长度列表,不能随便增删。
如果你需要的是可变 List,应该这样写:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); list.add("D");8.2 subList() 返回的是视图,不是副本
这个坑非常经典。
List<String> sub = oldList.subList(0, 2);很多人下意识会觉得 sub 是一个新的列表,但其实它只是原列表的一个视图。原集合结构一变,子集合就可能跟着出问题,甚至抛出 ConcurrentModificationException。
如果你想真正拷贝一份,正确做法是:
List<String> newList = new ArrayList<>(oldList.subList(0, 2));8.3 foreach 里不要直接删元素
错误写法:
for (String item : list) { if ("A".equals(item)) { list.remove(item); } }这类代码很容易触发 ConcurrentModificationException。
更稳妥的方式是 Iterator:
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("A".equals(item)) { iterator.remove(); } }8.4 Collectors.toMap() 很容易因为重复 key 报错
下面这段写法看起来很正常,但一旦 key 重复就会抛异常:
Map<String, Integer> map = list.stream() .collect(Collectors.toMap(User::getName, User::getAge));更稳的写法,是显式指定 merge 函数:
Map<String, Integer> map = list.stream() .collect(Collectors.toMap( User::getName, User::getAge, (oldValue, newValue) -> newValue ));另外,规范里还特别提醒过:如果 value 为 null,这里也可能出问题。
8.5 Collections.emptyList() 不能改
下面这种写法一样会出现问题:
List<String> list = Collections.emptyList(); list.add("Java");因为它返回的是不可变空集合。
如果你的语义只是“这里暂时没有数据”,它很好用;但如果你后面还要继续往里加元素,就不要这么写。
9. 一张表,帮你把集合选型记清楚
| 场景 | 推荐集合 |
|---|---|
| 普通有序列表 | ArrayList |
| 频繁头尾操作 | LinkedList / ArrayDeque |
| 去重 | HashSet |
| 保留插入顺序的去重 | LinkedHashSet |
| 自动排序去重 | TreeSet |
| 普通键值存储 | HashMap |
| 保持插入顺序的映射 | LinkedHashMap |
| 自动按 key 排序 | TreeMap |
| 并发键值存储 | ConcurrentHashMap |
如果你想把这一章真正学扎实,我建议至少把下面这套默认心智模型建立起来:
- 默认列表:ArrayList
- 默认去重:HashSet
- 默认映射:HashMap
- 并发映射:ConcurrentHashMap
- 需要排序:TreeMap / TreeSet