资讯动态

Java开发必备:CollectionUtils常用方法与避坑指南

发布时间:2026/10/15 14:39:33 来源:尧图企业网站定制
做Java开发这些年我发现一个规律越基础的类越容易写出又臭又长的重复代码集合判空就是最典型的一个。很多人一到业务代码里就条件反射地写一堆if (xxx ! null !xxx.isEmpty())而真正顺手的人会直接用 CollectionUtils 这类常用工具类。这篇文章我把 CollectionUtils 的各种常用方法从使用姿势到底层行为都翻一遍重点讲它和手写集合操作有什么本质区别、哪些方法有隐藏的坑、以及我在实际项目里踩过的版本冲突和集合修改问题。无论你是刚入行的新人还是写了好几年业务代码的老手只要还在用 Java 折腾集合这篇文章都值得花十分钟看完。1. 先搞清楚你用的是哪一个 CollectionUtils1.1 两个同名工具类的江湖地位很多人第一次接触 CollectionUtils 是在网上搜代码的时候结果一搜出来两个同名类一个是org.apache.commons.collections4.CollectionUtilsApache Commons Collections另一个是org.springframework.util.CollectionUtilsSpring 自带工具类。我第一次用的时候也被绕晕过明明 import 写好了代码里却报方法找不到后来才反应过来是引错了包。这两个类的定位完全不同。Apache Commons 那个是第三方开源库里的重量级选手方法极多集合运算、过滤、转换、判空应有尽有适合在业务代码里做各种集合处理。Spring 那个是为了满足框架自身需求而提供的一个轻量工具类方法不多主要就是判空、判断包含关系、数组合并这类基础操作但胜在零依赖项目里只要用了 Spring 就天然可用不需要额外引包。还有一个容易混淆的是 Google Guava 里的com.google.common.collect.Lists和Collections2它们也能完成类似操作但不是同一个体系。我见过不少人把 Guava 的Collections2.filter和 Apache 的CollectionUtils.filter混着用结果行为完全不同因为一个返回新集合一个原地修改集合后面我会专门讲这个坑。1.2 依赖坐标与包名对照先说 Apache Commons Collections 的依赖引入。以 Maven 为例比较新的项目直接用 4.x 版本dependency groupIdorg.apache.commons/groupId artifactIdcommons-collections4/artifactId version4.4/version /dependency这个坐标对应的包名是org.apache.commons.collections4注意后面这个4是带在包名里的。如果是老项目还在用 3.x那么坐标和包名就变成了commons-collections:commons-collections:3.2.2和org.apache.commons.collections。为什么这里我要专门说一句因为 3.x 和 4.x 的 API 虽然大部分方法名称一致但返回值、泛型支持、甚至个别方法的行为都做了调整混着 import 或者依赖版本冲突的时候编译器报的错误会让人非常头大。我整理了一个对照表维度Apache Commons Collections 3.xApache Commons Collections 4.xMaven 坐标commons-collections:commons-collections:3.2.2org.apache.commons:commons-collections4:4.4包名org.apache.commons.collectionsorg.apache.commons.collections4泛型支持基本没有返回裸类型全面泛型化filter 方法返回 void原地过滤返回 boolean同样原地过滤isEmpty 方法不提供提供 isEmpty / isNotEmpty如果你在项目里同时引入了这两个版本IDE 的自动导入往往会傻掉你得自己确认好到底用的是哪一个。Spring 的org.springframework.util.CollectionUtils同样需要注意它的方法数量有限没有 Apache 版那么强大但胜在不用额外引入依赖。2. Apache Commons 版 CollectionUtils 常用方法逐个过2.1 判空与空安全isEmpty / isNotEmpty我先从最常用的判空说起。以前在项目里经常看到的代码是这种ListString nameList queryNames(); if (nameList ! null nameList.size() 0) { // 业务处理 }这段代码本身没毛病但写得又长又容易遗漏。CollectionUtils.isEmpty帮你把 null 判断和空集合判断合并成了一个方法import org.apache.commons.collections4.CollectionUtils; ListString nameList queryNames(); if (CollectionUtils.isNotEmpty(nameList)) { // 业务处理 }这里重点说一下它的实现思路。isEmpty内部先判断集合是否为 null为 null 直接返回 true否则再判断collection.isEmpty()。也就是说它把你的! null判断省掉了一次性做到空安全。我见过不少同事第一次用的时候觉得这不过是语法糖但我的体会是集合判空是业务代码里出现频率最高的操作之一任何能减少重复的地方都值得用。2.2 集合运算union / intersection / subtract / disjunction这一组方法是我认为 CollectionUtils 最值钱的地方。你在业务代码里经常要算两个集合的并集、交集、差集自己写 for 循环嵌套效率低又容易出错用现成方法一行就搞定。ListInteger systemPerms Arrays.asList(1, 2, 3, 4, 5); ListInteger rolePerms Arrays.asList(3, 4, 5, 6, 7); // 并集两个集合全部元素去重 CollectionInteger union CollectionUtils.union(systemPerms, rolePerms); // 交集两个集合共有的元素 CollectionInteger intersection CollectionUtils.intersection(systemPerms, rolePerms); // 差集属于第一个集合但不在第二个集合里的元素 CollectionInteger diff CollectionUtils.subtract(systemPerms, rolePerms); // 对称差集只属于其中一个集合的元素去掉交集部分 CollectionInteger disjunction CollectionUtils.disjunction(systemPerms, rolePerms);我建议你在本地跑一下这段代码看输出结果。并集返回的是ArrayList元素顺序会按传入集合的迭代顺序组合交集和差集同样会返回新的集合不会改动原集合。这个方法在处理权限、标签、黑白名单这类业务时非常顺手后面实操章节我会给一个完整的权限场景。需要提醒的是这些方法都是基于迭代器实现对HashSet这类无序集合是安全的但如果你传入的是TreeSet且元素没有实现Comparable某些情况下可能会抛异常。反正我一般很少拿这些方法去处理TreeSet还是ArrayList和HashSet用得稳。2.3 包含关系与查找containsAny / containsAll / find / countMatches集合包含关系的判断也是高频需求。以前判断两个集合是否有交集我见过太多人写双层循环后来用了containsAny发现原来一行就够ListString blacklist Arrays.asList(ip-001, ip-002, ip-003); ListString requestIps Arrays.asList(ip-002, ip-009); if (CollectionUtils.containsAny(requestIps, blacklist)) { // 命中黑名单 }containsAny判断第一个集合里是否有任意一个元素出现在第二个集合中等价于交集不为空。containsAll则更严格要求第一个集合包含第二个集合的所有元素等价于第二个集合是第一个集合的子集。在查找元素方面4.x 提供了find方法可以按条件返回第一个匹配的元素Order targetOrder CollectionUtils.find(orderList, order - A10001.equals(order.getOrderNo()));如果找不到就返回 null。这个方法省去写 for 循环加 break 的琐碎代码。另外还有一个countMatches用来统计集合中满足条件的元素个数int highAmountCount CollectionUtils.countMatches(orderList, order - order.getAmount() 1000);有一点值得注意find和countMatches在 4.x 中接收的是Iterable而不是Collection所以传入List、Set都可以甚至自定义的Iterable也能用。2.4 过滤、筛选与转换filter / select / selectRejected / transform这组方法是最容易踩坑的地方我单独拿出来说。filter的作用是按条件过滤集合但它有一个迷惑性极强的行为它会直接修改你传入的那个集合而不是返回一个新集合。4.x 版本中它的返回值只是一个 boolean表示是否过滤掉了至少一个元素ListOrder pendingOrders new ArrayList(); // 填充数据... // 注意filter 会原地删除 pendingOrders 中不满足条件的元素 CollectionUtils.filter(pendingOrders, order - PAID.equals(order.getStatus()));我见过有同事把 filter 的返回值当成新集合用结果发现原集合被改掉了业务数据莫名其妙少了一部分排查了半天。如果你希望保留原集合那就用select它会返回一个新的集合原集合不变// 返回一个新集合只包含满足条件的元素 CollectionOrder paidOrders CollectionUtils.select(orderList, order - PAID.equals(order.getStatus())); // 返回一个新集合只包含不满足条件的元素 CollectionOrder unpaidOrders CollectionUtils.selectRejected(orderList, order - PAID.equals(order.getStatus()));还有transform这个方法用来对集合中的每个元素做转换同样是原地修改ListLong orderIds new ArrayList(Arrays.asList(1L, 2L, 3L)); // 每个元素乘以1000结果会写回原集合 CollectionUtils.transform(orderIds, id - id * 1000L);4.x 的transform要求Transformer的输入输出类型一致因为要直接替换集合中的元素。如果你想把ListString变成ListInteger那就不能靠 transform 了得自己用 Java 8 的stream().map()或者遍历后逐个转换这个后面在实操部分会给出具体例子。2.5 一些好用但容易忽略的方法addIgnoreNull / isEqualCollection / collate这几个方法在文档里存在感不高但某些场景下非常好用。addIgnoreNull往集合里添加元素时自动忽略 null 值ListString tagList new ArrayList(); CollectionUtils.addIgnoreNull(tagList, vue); CollectionUtils.addIgnoreNull(tagList, null); CollectionUtils.addIgnoreNull(tagList, java); // 最终 tagList 只有 [vue, java]以前写代码要判断元素不是 null 再加入用这个方法又少写了一层 if。isEqualCollection比较两个集合是否包含相同元素无视顺序例如[1,2,3]和[3,2,1]会被判定为相等。这个和List.equals不同List.equals是要求顺序一致的isEqualCollection只关心元素集合是否一致。想判断两个列表内容相同但不管顺序时这个方法非常省事。collate则是把两个已排序的集合合并成一个新的有序列表有点类似于归并排序ListInteger listA Arrays.asList(1, 3, 5, 7); ListInteger listB Arrays.asList(2, 4, 6, 8); ListInteger merged CollectionUtils.collate(listA, listB); // merged: [1, 2, 3, 4, 5, 6, 7, 8]注意这里要求两个输入集合本身是有序的否则结果会乱套。我一般用它做分页数据的合并比如两个接口分别返回了按时间倒序的订单列表合并后再统一排序比手动插入高效。3. 实操过程从依赖到三个真实场景3.1 引入依赖和第一段验证代码实操第一步先把依赖确认好。我建议现在的项目里统一使用 4.x不仅泛型安全方法也更齐全。引入依赖之后先写一个最简单的小测试验证一下环境是否正常import org.apache.commons.collections4.CollectionUtils; import java.util.Arrays; import java.util.List; public class CollectionUtilsDemo { public static void main(String[] args) { ListString skillList Arrays.asList(java, spring, mysql); if (CollectionUtils.isNotEmpty(skillList)) { System.out.println(集合不为空元素个数 skillList.size()); } } }如果这段代码能跑通说明依赖和包路径都没问题。这里有一个关键点如果你发现编译期提示找不到CollectionUtils大概率是三个原因一是 Maven 依赖没有刷新下来二是引入了旧版 3.x 但在代码里写的是 4 的包名三是项目里另有一个同名的类把 import 给搞混了排查方向先往这三个地方找。3.2 场景一订单列表筛选与状态分组我拿一个实际业务场景来串一下常用方法。假设某个后台管理系统里有一个订单列表接口前端需要按状态分组展示同时还要统计某个金额区间内的订单数量。第一件事是筛选出已支付的订单同时保留原始列表用于其他逻辑这时候就不要用filter而是用selectListOrder orderList orderService.queryAll(); // 筛选已支付订单原列表不受影响 CollectionOrder paidOrders CollectionUtils.select(orderList, order - PAID.equals(order.getStatus())); // 统计金额大于 500 的订单数量 int bigOrderCount CollectionUtils.countMatches(orderList, order - order.getAmount() 500);如果某个需求要求把已支付的订单从原集合里移除不太介意原集合被修改再考虑用filter。我自己的原则是能不动原集合就尽量不动原地修改在复杂业务里容易引发隐藏 bug。3.3 场景二权限集合的交并差运算权限校验是集合运算最典型的应用场景。假设用户拥有自己的角色权限集合又要判断用户是否拥有某个特定资源权限以及计算最终可访问的权限范围这时候用交集和并集就非常直观SetString userRoles new HashSet(Arrays.asList(ADMIN, USER, VIEWER)); SetString resourceRequiredRoles new HashSet(Arrays.asList(ADMIN, MANAGER)); // 判断用户角色是否与资源所需角色有交集 if (CollectionUtils.containsAny(userRoles, resourceRequiredRoles)) { // 允许访问 } // 计算这两个角色集合的并集用于后续展示 CollectionString totalRoles CollectionUtils.union(userRoles, resourceRequiredRoles); // 计算用户没有的角色用于提示哪些资源无权访问 CollectionString missingRoles CollectionUtils.subtract(resourceRequiredRoles, userRoles);这段代码比用两层 for 循环判断要清晰太多。union返回去重后的并集subtract返回在第二个集合但不在第一个集合的元素正好用来算权限缺口。我在权限系统里经常把这类逻辑抽成一个小的静态方法整个模块的代码可读性提升非常明显。3.4 场景三Spring CollectionUtils 的日常用法如果你的项目本身就在用 Spring那么org.springframework.util.CollectionUtils在某些场景下更顺手因为它完全不需要引入额外的依赖。Spring 版同样有isEmpty和isEmpty的重载一个支持Collection一个支持Mapimport org.springframework.util.CollectionUtils; MapString, Object configMap configService.getConfigMap(); if (!CollectionUtils.isEmpty(configMap)) { // 配置不为空才执行逻辑 }自己写Map判空的代码通常长这样map ! null !map.isEmpty()。用 Spring 的CollectionUtils一行就能搞定而且你不需要担心项目里有没有引入 commons-collections4。Spring 版还有一个containsAny方法用法和 Apache 版一致boolean hit CollectionUtils.containsAny(sourceList, targetList);此外还有一个mergeArrayIntoCollection可以把数组元素合并进一个已有的集合String[] extras {v1, v2}; ListString mainList new ArrayList(Arrays.asList(base)); CollectionUtils.mergeArrayIntoCollection(extras, mainList); // mainList: [base, v1, v2]这个在写兼容接口或者搭数据时很实用。我的建议是如果项目里已经用了 Apache Commons Collections那集合运算、过滤转换这些重量级操作优先用 Apache 版如果不想引入依赖只想做做判空、判断包含那 Spring 版就够了。两个同时存在也没有冲突只要包名写对就行。4. 常见问题与排查技巧实录4.1 找不到类或方法典型的依赖缺失或版本冲突这个报错我见得最多报错形式通常有两种Cannot resolve symbol CollectionUtils或者No such method error。如果是编译期找不到类先去检查 Maven 依赖有没有引进来。如果你确实引了依赖但还是报错检查一下项目里是不是有多个模块每个模块的依赖情况不一样。还有一种诡异情况是 IDE 的缓存问题强制刷新 Maven 再重启就解决了。如果是运行期报方法不存在基本可以断定是 jar 包版本不对。比如代码里用了 4.x 的CollectionUtils.isEmpty但实际依赖的是 3.2.2 的 jar而这个 jar 里根本没有这个方法自然就报NoSuchMethodError。排查方式就是看依赖树mvn dependency:tree -Dincludescommons-collections如果看到多个版本的commons-collections和commons-collections4同时存在赶紧在对应模块里排除掉不需要的那个。这都是我在实际项目里被坑出来的经验。4.2 filter 的原地修改陷阱前面已经反复强调过filter是原地修改集合不返回新集合。这是 Apache Commons Collections 里最容易让人误解的方法之一我甚至见过有人专门写博客吐槽这个设计。我自己的经验是在使用filter之前先问自己一句这个集合被其他地方引用了吗如果集合是从某个共享内存、缓存或者类成员变量里拿出来的用了filter之后其他代码可能就看不到原有数据了。除非你确定这个集合是一次性使用的局部变量否则我建议优先用select让代码意图更清晰CollectionOrder result CollectionUtils.select(originalList, predicate);后面排查问题时第一步不是看业务逻辑而是检查有没有人对源集合做过原地过滤操作。有一次一个报表数据总是不对排查到后面发现就是某个工具类方法顺手把查询出来的集合给改了。4.3 不可变集合导致 UnsupportedOperationException用Arrays.asList创建的列表是一个不可变列表长度固定不能进行结构性修改。如果你对它调用filter、transform、addIgnoreNull运行时会抛出UnsupportedOperationException。我举一个典型的错误写法ListString tagList Arrays.asList(java, python, php); CollectionUtils.filter(tagList, tag - !php.equals(tag)); // 运行时报错java.lang.UnsupportedOperationException原因是Arrays.asList返回的是一个内部类ArrayList它的底层虽然是数组但没有实现修改结构的方法。要使用过滤或转换必须先包装成java.util.ArrayListListString tagList new ArrayList(Arrays.asList(java, python, php)); CollectionUtils.filter(tagList, tag - !php.equals(tag));这个错误极其常见别问我怎么知道的。写完代码之后最好带着这条规则自查一遍凡是准备传给 CollectionUtils 做修改操作的集合务必确保它是new ArrayList、new HashSet或者其他可变集合千万不能是Arrays.asList的产物。4.4 泛型与类型转换Apache Commons Collections 4.x 已经全面支持泛型但这个改进也带来了一些编译期的约束。举个直观的例子transform要求传入的Transformer输入输出类型一致所以下面这种代码编译就会报错// 编译不通过类型不一致 ListString codeList new ArrayList(); CollectionUtils.transform(codeList, code - Integer.parseInt(code));如果你确实要做String到Integer的转换最直接的方式是用 Java 8 的 StreamListInteger valueList codeList.stream() .map(Integer::parseInt) .collect(Collectors.toList());另外 4.x 的Predicate接口虽然只有一个抽象方法evaluate但它并没有标注FunctionalInterface不过由于它是一个标准的函数式接口所以 lambada 表达式依然可以直接传入。如果你遇到编译期提示方法引用不兼容先看一下是不是泛型推断出了问题显式写出类型参数通常能解决CollectionUtils.select(orderList, (PredicateOrder) order - PAID.equals(order.getStatus()));4.5 性能考量工具类不是万金油最后说一个容易被忽略的问题。CollectionUtils 确实方便但它内部实现是基于迭代器的并没有针对大规模数据做专门优化。如果你要处理的是十万、百万级别的集合containsAny这类方法的时间复杂度是 O(n*m)老老实实把集合转成HashSet再判断才是正解。我处理过一个大数据量场景当时用CollectionUtils.containsAny判断两个上万条数据的列表是否有交集耗时肉眼可见地卡顿改成先构建HashSet再遍历匹配后性能提升非常明显。工具类追求的是代码简洁和可读性在数据量有限、不是热点的业务路径上大胆用一旦涉及到并发、大数据量、高性能要求还是要回归到最基础的数据结构和算法思维。另外方法的命名也容易产生误导。有些人觉得CollectionUtils.union效率一定比手写循环高其实它本质就是循环遍历加去重性能并不会更好。工具类的价值在于少写代码、降低出错概率而不是替你优化算法。我用了这几年 CollectionUtils最大的体会是这类工具类真正解决的问题不是性能而是让业务代码从满屏的for循环和 if 判断里解放出来。工作中喜欢把集合操作写得很啰嗦的人往往不是不会用工具类而是没意识到自己写的每一段重复代码都在埋坑。如果你刚开始用我建议先掌握isEmpty、isNotEmpty、select、containsAny、union这几个最常用的方法足够覆盖日常八成的需求。等用熟练了再去挖掘disjunction、collate这些偏门方法。最后再分享一个小技巧写工具类方法时优先选择返回新集合而不是修改原集合的方式这条原则不仅能帮你避开 CollectionUtils 里的大部分的坑也能让你写业务代码时少很多莫名其妙的 bug我实践下来非常管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑