写 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这张图不用全背但你至少要意识到一件事工程里最常打交道的集合并没有那么多。真正的主角其实长期都是下面这几个ArrayListHashSetHashMapTreeMapConcurrentHashMap把这几个吃透集合这章基本就站住了。2. List、Set、Map 到底怎么选别再凭感觉了很多初学者写代码时选集合基本靠手感。想到列表就 List想到键值对就 Map剩下的能跑就行。短期看没问题长期看会吃亏。2.1 需要顺序、允许重复用 List只要你的数据需要“按顺序放着”并且允许重复优先考虑 List。典型场景数据库查出来的一批用户页面上的商品列表按时间顺序记录的操作日志而在 List 里最常用的两个实现类是ArrayListLinkedList但这里有个很重要的结论大多数业务场景里默认优先 ArrayList。2.2 需要去重用 Set如果你的核心需求不是“存一批数据”而是“确保数据不重复”那就该想到 Set。比如一批手机号去重用户标签去重判断某个元素是否已经存在常见实现类HashSet最常用去重快LinkedHashSet去重的同时保留插入顺序TreeSet自动排序去重2.3 需要映射关系用 Map只要数据天然是“一个 key 对应一个 value”就优先用 Map。典型场景用户 ID 对应用户对象配置项名称对应配置值单词对应出现次数常见实现类HashMapLinkedHashMapTreeMapConcurrentHashMap如果你现在只能记一条经验那就记这一句默认列表选 ArrayList默认映射选 HashMap默认去重选 HashSet。3. 为什么大家都在用 ArrayList而不是 LinkedList这是集合里最典型的“看上去会实际上容易答偏”的问题。很多人第一反应是链表插入删除快所以 LinkedList 应该更适合业务开发。这个说法只说对了一半。3.1 ArrayList 的底层是动态数组ArrayList 底层是动态数组所以它的特点非常鲜明支持随机访问get(index) 很快尾部追加元素效率高中间插入、删除需要移动元素容量不够时会扩容示例ListString 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它允许 nullHashMap允许 null key允许 null value但 ConcurrentHashMap 不允许这是面试里的高频对比点。2已知数据量时尽量初始化容量如果你大概知道要放多少元素创建时最好顺手指定容量避免频繁扩容。MapString, Integer map new HashMap(16);在工程规范里这也是一个很实用的优化习惯。3遍历时优先 entrySet遍历 Map 时优先用 entrySet()不要总是先拿 keySet() 再去 get()。for (Map.EntryString, Integer entry : map.entrySet()) { System.out.println(entry.getKey() - entry.getValue()); }6. TreeMap 和 TreeSet 不是冷门它们只是有明确适用场景很多人学集合时会把 TreeMap、TreeSet 当成“知道有这个东西就行”其实没那么简单。它们底层基于红黑树最大的特点就是自动排序支持有序遍历适合范围查询比如这些场景就很合适按分数排序按日期维护数据需要找“大于某个值的最小 key”示例MapInteger, String treeMap new TreeMap(); treeMap.put(3, C); treeMap.put(1, A); treeMap.put(2, B); System.out.println(treeMap); // {1A, 2B, 3C}注意TreeMap 和 TreeSet 要么依赖元素实现 Comparable要么创建时显式传入 Comparator。7. 并发环境下别再默认用 HashMap 了单线程环境用 HashMap 很正常但只要你进入并发读写场景就不能继续想当然了。这时候更合适的选择通常是 ConcurrentHashMap。MapString, 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很多人会这样写ListString list Arrays.asList(A, B, C); list.add(D);然后程序直接报错。原因是 Arrays.asList() 返回的是固定长度列表不能随便增删。如果你需要的是可变 List应该这样写ListString list new ArrayList(Arrays.asList(A, B, C)); list.add(D);8.2 subList() 返回的是视图不是副本这个坑非常经典。ListString sub oldList.subList(0, 2);很多人下意识会觉得 sub 是一个新的列表但其实它只是原列表的一个视图。原集合结构一变子集合就可能跟着出问题甚至抛出 ConcurrentModificationException。如果你想真正拷贝一份正确做法是ListString newList new ArrayList(oldList.subList(0, 2));8.3 foreach 里不要直接删元素错误写法for (String item : list) { if (A.equals(item)) { list.remove(item); } }这类代码很容易触发 ConcurrentModificationException。更稳妥的方式是 IteratorIteratorString iterator list.iterator(); while (iterator.hasNext()) { String item iterator.next(); if (A.equals(item)) { iterator.remove(); } }8.4 Collectors.toMap() 很容易因为重复 key 报错下面这段写法看起来很正常但一旦 key 重复就会抛异常MapString, Integer map list.stream() .collect(Collectors.toMap(User::getName, User::getAge));更稳的写法是显式指定 merge 函数MapString, Integer map list.stream() .collect(Collectors.toMap( User::getName, User::getAge, (oldValue, newValue) - newValue ));另外规范里还特别提醒过如果 value 为 null这里也可能出问题。8.5 Collections.emptyList() 不能改下面这种写法一样会出现问题ListString list Collections.emptyList(); list.add(Java);因为它返回的是不可变空集合。如果你的语义只是“这里暂时没有数据”它很好用但如果你后面还要继续往里加元素就不要这么写。9. 一张表帮你把集合选型记清楚场景推荐集合普通有序列表ArrayList频繁头尾操作LinkedList / ArrayDeque去重HashSet保留插入顺序的去重LinkedHashSet自动排序去重TreeSet普通键值存储HashMap保持插入顺序的映射LinkedHashMap自动按 key 排序TreeMap并发键值存储ConcurrentHashMap如果你想把这一章真正学扎实我建议至少把下面这套默认心智模型建立起来默认列表ArrayList默认去重HashSet默认映射HashMap并发映射ConcurrentHashMap需要排序TreeMap / TreeSet