Java 集合工具类实战手册:CollectionUtils 与 MapUtils
本文所有 API 均已对照 Apache Commons Collections 4 官方 Javadoc(4.6.0)逐条核验,废弃方法会明确标注,并给出官方推荐的替代写法。文末附「五个必踩的坑」和与 Guava 的对照表。
一、为什么还需要它
JDK 8 之后我们有 Stream,很多集合操作确实一行就能写完。但 Stream 有三个先天短板,正好是这个库的立身之本:
- Stream 不 null 安全 。
list.stream()在list == null时直接 NPE,而业务代码里「上游可能返回 null」太常见了,于是到处都是if (coll != null && !coll.isEmpty())。 - 集合运算要自己写 。交集、并集、差集、对称差集,Stream 得先
collect成 Set 再做retainAll,啰嗦且容易写出 bug。 - 分批没有原生支持 。
ListUtils.partition()这种「按 500 条切一刀」的需求,JDK 至今没有对应方法。
Commons Collections 4 的定位不是取代 Stream,而是补齐 Stream 不管的那些脏活------尤其是判空、集合代数运算和分批。
二、引入依赖
xml
<!-- Maven -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.6.0</version>
</dependency>
groovy
// Gradle
implementation 'org.apache.commons:commons-collections4:4.6.0'
4.6.0 是当前最新版本(2026-08-06 发布)。老项目里常见的 4.4 也完全够用,本文涉及的所有 API 在 4.4 中均已存在。最低要求 Java 8。
注意包名是 org.apache.commons.collections4(带 4),和已停止维护的 commons-collections 3.x 不是同一个坐标,两者可以共存但别混用。
java
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.collections4.SetUtils;
下面统一用这两个示例集合演示:
java
List<String> a = List.of("x", "y", "z");
List<String> b = List.of("y", "z", "w");
三、CollectionUtils:主力工具类
3.1 判空:日常出场率最高
java
CollectionUtils.isEmpty(null); // true
CollectionUtils.isEmpty(List.of()); // true
CollectionUtils.isNotEmpty(a); // true
MapUtils.isEmpty(map); // Map 用 MapUtils
MapUtils.isNotEmpty(map);
两个方法都是 null 安全的,null 和空集合一律返回 true。这一条就能把满屏的 if (coll != null && !coll.isEmpty()) 收敛成一行。
3.2 集合运算
java
CollectionUtils.intersection(a, b); // [y, z] 交集
CollectionUtils.union(a, b); // [x, y, z, w] 并集
CollectionUtils.subtract(a, b); // [x] 差集 a - b
CollectionUtils.disjunction(a, b); // [x, w] 对称差集
CollectionUtils.containsAny(a, b); // true 是否有公共元素
CollectionUtils.isSubCollection(a, b); // a 是否为 b 的子集
CollectionUtils.isEqualCollection(a, b); // 忽略顺序判断相等
四个方法都返回新建的 Collection<O>,不修改入参。
这里有个容易被忽略的语义细节 :这套运算不是「去重的集合代数」,而是**基数感知(cardinality-aware)**的,行为更接近多重集(bag)。官方文档明确给出了 disjunction 的基数公式:
cardinality(e) = max(cardinality(e, a), cardinality(e, b))
- min(cardinality(e, a), cardinality(e, b))
并说明它等价于 subtract(union(a, b), intersection(a, b))。由此可知 union 取两者基数的较大值 、intersection 取较小值,而不是简单相加。
举个会踩到的例子:
java
List<String> l1 = List.of("a", "a", "b");
List<String> l2 = List.of("a", "c");
CollectionUtils.union(l1, l2); // [a, a, b, c] ------ max(2,1)=2 个 a
ListUtils.union(l1, l2); // [a, a, b, a, c] ------ 纯拼接,3 个 a
两个都叫 union,结果完全不同。选哪个取决于你要的是「合并去重按最大基数」还是「顺序拼接」。
另外,isEqualCollection 不只比较元素是否相同,还比较每个元素出现的次数 ,所以 [a, a, b] 和 [a, b, b] 判定为不相等。
3.3 筛选与转换
java
// 按条件筛选,返回新集合
Collection<String> filtered = CollectionUtils.select(a, s -> s.startsWith("x"));
// 反向筛选。⚠️ 方法名是 selectRejected,没有 reject 这个方法
Collection<String> rejected = CollectionUtils.selectRejected(a, s -> s.startsWith("x"));
// 类型转换
Collection<Integer> lengths = CollectionUtils.collect(a, String::length);
select / selectRejected 都是新建集合返回,不动原集合。如果你想原地过滤,用这两个未废弃的方法:
java
CollectionUtils.filter(list, s -> s.startsWith("x")); // 原地保留匹配项,返回是否有变更
CollectionUtils.filterInverse(list, s -> s.startsWith("x")); // 原地剔除匹配项
一次遍历同时分成两堆,用四参数重载,比调两次 select 省一遍循环:
java
List<String> pass = new ArrayList<>();
List<String> fail = new ArrayList<>();
CollectionUtils.select(source, predicate, pass, fail);
查找单个元素:
java
// ⚠️ 已废弃(Since 4.1),改用 IterableUtils.find
// CollectionUtils.find(a, s -> s.equals("y"));
IterableUtils.find(a, s -> s.equals("y")); // 找不到返回 null,不抛异常
3.4 计数与按下标取值
这两个方法是废弃重灾区,直接给正确写法:
java
// ❌ 错误:CollectionUtils.countMatches 没有「传元素」的重载,参数是 Predicate;
// 且该方法自 4.1 起已废弃
// CollectionUtils.countMatches(a, "y");
// ✅ 正确写法,注意返回类型是 long
long times = IterableUtils.countMatches(a, s -> s.equals("y")); // 1
java
// ⚠️ CollectionUtils.get(Iterable, int) 自 4.1 起已废弃,改用 IterableUtils.get
// ⚠️ 更重要的:越界会抛 IndexOutOfBoundsException,不是返回 null
IterableUtils.get(a, 1); // "y"
统计元素出现次数还有一个经典陷阱:
java
IterableUtils.frequency(a, "y"); // 1,参数顺序是 (集合, 元素)
// ❌ CollectionUtils.cardinality 虽仍存在但已废弃,且【参数顺序相反】:(元素, 集合)
// CollectionUtils.cardinality("y", a);
官方在 cardinality 的废弃说明里专门写了一句「Be aware that the order of parameters has changed」------参数顺序换了。这种迁移如果只改类名不改参数顺序,编译能过、结果全错,属于最难查的一类 bug。
3.5 其他实用方法
java
CollectionUtils.size(a); // 支持 数组 / Collection / Map / Iterator / Enumeration
CollectionUtils.addAll(list, array);
CollectionUtils.reverseArray(arr); // 数组原地反转,注意入参类型是 Object[]
CollectionUtils.emptyIfNull(coll); // null → 空的不可变集合
CollectionUtils.collate(sorted1, sorted2); // 归并两个【已排序】集合,O(n)
size(Object) 是个很省心的方法:它接受 Object,内部自动判断是数组、集合还是迭代器,不用为不同类型写不同的取长度逻辑。
reverseArray 的入参声明是 Object[],所以 int[] 这类基本类型数组传不进去。
四、ListUtils:分批是刚需
java
ListUtils.partition(bigList, 1000); // 分批!批量插入 / 批量调 API 必备
ListUtils.emptyIfNull(list); // null → 空集合
ListUtils.unmodifiableList(list); // 不可变视图
ListUtils.defaultIfNull(list, fallback);
ListUtils.getFirst(list); // 4.5.0+
ListUtils.getLast(list); // 4.5.0+
ListUtils.indexOf(list, predicate); // 按条件找下标
ListUtils.longestCommonSubsequence(l1, l2); // 最长公共子序列
典型用法:
java
for (List<Long> batch : ListUtils.partition(userIds, 500)) {
userRepository.findByIdIn(batch); // 避免 IN 子句过长
}
partition 返回的是视图,不是新列表。 官方文档原文:
The outer list is unmodifiable, but reflects the latest state of the source list. The inner lists are sublist views of the original list, produced on demand using
List.subList(int, int).
拆开说三层含义:
- 外层
List<List<T>>不可修改,往里add会抛UnsupportedOperationException; - 内层每个批次是原列表的
subList视图,原列表内容变了,批次内容跟着变; - 批次是按需生成的,所以
partition一个百万元素的列表也不会立刻占用额外内存。
好处是省内存,代价是:如果你在遍历批次的同时修改了源列表,会得到诡异结果。需要独立快照就先 new ArrayList<>(batch) 拷一份。
另外两个方法:
java
ListUtils.removeAll(l1, l2); // 返回【新列表】,不修改 l1;l2 中元素的重复出现会全部移除
ListUtils.retainAll(l1, l2); // 同理,返回新列表
ListUtils.intersection(l1, l2);
ListUtils.subtract(l1, l2);
ListUtils.sum(l1, l2); // union 减去 intersection
removeAll 的价值在于「不想动原集合」------原生 Collection.removeAll() 是原地修改的,官方文档也点明了这个方法就是为了替代它。
java
// ⚠️ ListUtils.union 不是数学意义上的并集!
ListUtils.union(l1, l2); // 语义是「把第二个列表拼接到第一个之后」,保留全部重复元素
五、SetUtils:返回视图,省内存
java
SetUtils.union(set1, set2); // 并集视图
SetUtils.intersection(set1, set2); // 交集视图
SetUtils.difference(set1, set2); // 差集视图 a \ b
SetUtils.disjunction(set1, set2); // 对称差集视图
SetUtils.difference(set1, set2).toSet(); // 需要实体集合时才 toSet()
四个运算方法返回的都是 SetUtils.SetView<E>------一个不可修改的视图 ,不新建集合。SetView 继承 AbstractSet 并实现 Set 接口,可以直接当 Set 传给下游方法;但它是 unmodifiable 的,调用 add / remove 会抛 UnsupportedOperationException。
只有确实需要一个可修改的实体集合时,才调 toSet()。SetView 还提供了 copyInto(targetSet),可以把内容直接灌进你指定的 Set 实现里。
其余常用方法:
java
SetUtils.emptyIfNull(set);
SetUtils.hashSet("a", "b", "c"); // 4.3+,可变参数直接建 HashSet
SetUtils.newIdentityHashSet(); // 用 == 而非 equals 比较的 Set
SetUtils.orderedSet(set); // 保持插入顺序
SetUtils.unmodifiableSet(set);
SetUtils.synchronizedSet(set);
SetUtils.predicatedSet(set, predicate); // 写入时校验
SetUtils.transformedSet(set, transformer); // 写入时转换
六、MapUtils:类型安全取值
处理 Map<String, Object> 这种弱类型结构(读 JSON、读配置、接第三方接口)时,这批方法能省掉大量强转和判空:
java
Map<String, Object> map = Map.of("name", "张三", "age", 28, "score", 95.5);
MapUtils.getString(map, "name"); // "张三"
MapUtils.getString(map, "missing", "默认"); // "默认",取不到返回默认值
MapUtils.getInteger(map, "age"); // 28,自动类型转换
MapUtils.getIntValue(map, "age", 0); // 28,取不到返回默认值
MapUtils.getDouble(map, "score"); // 95.5
MapUtils.getBoolean(map, "flag");
MapUtils.isEmpty(map);
MapUtils.isNotEmpty(map);
getXxx 返回包装类型(可能为 null),getXxxValue 返回基本类型(取不到时给默认值或 0),按需选。
网上流传「4.x 里 MapUtils 的 getXxx 系列都废弃了」------这是误解 。经核对官方 Javadoc,getString / getInteger / getDouble / getBoolean / getIntValue 这批方法均未标注 @Deprecated,可以放心用。MapUtils 中唯一被废弃的是三个 multiValueMap(...) 重载(Since 4.1,官方建议改用 MultiValuedMap)。
其余实用方法:
java
MapUtils.getObject(map, key); // 泛型安全取值
MapUtils.emptyIfNull(map); // null → 空 Map
MapUtils.size(map); // null 安全的 size
MapUtils.invertMap(map); // 键值反转
MapUtils.unmodifiableMap(map);
MapUtils.synchronizedMap(map);
MapUtils.lazyMap(map, factory); // 取值时惰性填充
MapUtils.predicatedMap(map, keyPred, valPred); // 写入校验
MapUtils.transformedMap(map, keyTrans, valTrans); // 写入转换
lazyMap 很适合做「缓存缺失时自动计算」的场景,比手写 computeIfAbsent 更能统一风格。
七、五个必踩的坑
这一节是本文的核心,把上面散落的风险点集中列一遍。
坑 1:countMatches 传元素
java
CollectionUtils.countMatches(a, "y"); // ❌ 编译不过
countMatches 的第二个参数是 Predicate,不是元素。而且它自 4.1 起已废弃。正确写法是 IterableUtils.countMatches(a, s -> s.equals("y")),返回 long。
坑 2:以为 get 越界返回 null
CollectionUtils.get() / IterableUtils.get() 越界时抛的是 IndexOutOfBoundsException。想安全取值请自己判长度,或用 ListUtils.partition 之类不依赖下标的方式。同时注意 CollectionUtils.get(Iterable, int) 已废弃,改用 IterableUtils.get。
坑 3:cardinality → frequency 参数顺序反了
java
CollectionUtils.cardinality("y", a); // ❌ 已废弃,(元素, 集合)
IterableUtils.frequency(a, "y"); // ✅ (集合, 元素)
迁移时只改类名不改参数顺序,代码能编译但结果错。这是官方废弃说明里唯一专门加了警告的一句。
坑 4:两个 union 语义不同
CollectionUtils.union 是基数感知的(取 max),ListUtils.union 是纯拼接。名字一样,行为完全不同。需要真正的「拼接」用 ListUtils.union,需要「合并」用 CollectionUtils.union。
坑 5:partition 的视图特性
内层批次是源列表的 subList 视图,边遍历边改源列表会出问题。需要独立副本时显式拷贝。
顺带确认两个笔记里写对了的点(这两个确实容易记错):
- 反向筛选的方法名是
selectRejected,没有reject; SetUtils的运算返回视图,toSet()才落地成实体集合。
八、与 Guava 对照
很多项目里两个库都有,知道对应关系能少查文档:
| 需求 | Commons Collections 4 | Guava |
|---|---|---|
| 集合判空 | CollectionUtils.isEmpty() |
Iterables.isEmpty() |
| 分批 | ListUtils.partition() |
Lists.partition() |
| 交集 | CollectionUtils.intersection() |
Sets.intersection() |
| 并集 | CollectionUtils.union() |
Sets.union() |
| 差集 | CollectionUtils.subtract() |
Sets.difference() |
| 对称差集 | CollectionUtils.disjunction() |
Sets.symmetricDifference() |
| 元素计数 | IterableUtils.frequency() |
Collections.frequency() |
| Map 判空 | MapUtils.isEmpty() |
Maps 无对应,用原生 |
Guava 的 Sets 系列同样返回视图,语义和 SetUtils 接近。选型上不用纠结:项目里已经有哪个就用哪个,别为了一个 partition 再引一个库。
九、速查表
| 类 | 高频方法 | 备注 |
|---|---|---|
CollectionUtils |
isEmpty / isNotEmpty |
null 安全 |
select / selectRejected |
新建集合返回 | |
filter / filterInverse |
原地修改 | |
collect |
类型转换 | |
intersection / union / subtract / disjunction |
基数感知 | |
size(Object) |
通吃数组/集合/Map/迭代器 | |
IterableUtils |
find / frequency / countMatches / get |
从 CollectionUtils 迁移过来的这批方法都在这个类 |
ListUtils |
partition |
返回视图 |
removeAll / retainAll |
返回新列表,不改原集合 | |
emptyIfNull |
null 安全 | |
SetUtils |
union / intersection / difference / disjunction |
返回 SetView 视图 |
hashSet / newIdentityHashSet |
快速建集合 | |
MapUtils |
getString / getInteger / getIntValue 等 |
弱类型 Map 取值,未废弃 |
invertMap / lazyMap |
一句话总结:判空、集合代数、分批,这三件事交给 Commons Collections 4;复杂的流式转换、聚合、分组,还是 Stream 更顺手。两者不是替代关系。
本文 API 核验基于 Apache Commons Collections 4.6.0 官方 Javadoc。