• 如何借助Hutool的CollUtil工具类规避NPE风险
  • 发布于 1天前
  • 55 热度
    0 评论
传统集合处理中,频繁编写if-else判空、手动循环实现去重与交集运算,不仅导致代码冗余,还易引发空指针异常(NPE),降低开发效率与代码可维护性。我们可借助Hutool的CollUtil工具类,通过其封装的API统一集合操作逻辑,大幅减少重复代码,同时规避NPE风险。下文将从核心痛点、CollUtil关键能力及项目实战角度,详细讲解其应用。

1. 传统集合处理的核心痛点

.传统集合处理需手动实现判空、筛选、去重等操作,步骤繁琐且易出问题。

.判空需重复编写if (list == null || list.isEmpty()),多集合判空时代码堆积严重。

.去重需创建Set并循环添加元素,自定义对象还需重写equals方法,操作复杂。
.求交集需双重循环比对,不仅性能低,还易遗漏边界判断。
这些操作共同导致代码冗余,且NPE、数据覆盖等风险高发。
// 传统集合处理:筛选启用用户、去重、求交集
public  List<Long> getValidUserIds(List<User> userList, List<Long> roleAuthUserIds) {
    // 1. 判空用户列表,避免NPE,空则返回空集合  
    if (userList == null || userList.isEmpty()) {
        return  Collections.emptyList();
    }
    // 堆代码 duidaima.com
    // 2. 判空权限ID列表,避免NPE,空则返回空集合  
    if (roleAuthUserIds == null || roleAuthUserIds.isEmpty()) {
        return  Collections.emptyList();
    }
    // 3. 创建列表存储启用的用户  
    List<User> activeUsers = new   ArrayList<>();
    // 4. 循环遍历用户列表,筛选启用的非空用户  
    for (User user : userList) {
        if (user != null && user.isActive()) {
            activeUsers.add(user);
        }
    }
    // 5. 创建Set存储去重后的用户ID  
    Set<Long> activeUserIds = new   HashSet<>();
    // 6. 循环提取启用用户的ID,利用Set实现去重  
    for (User user : activeUsers) {
        activeUserIds.add(user.getId());
    }
    // 7. 创建列表存储最终有效的用户ID  
    List<Long> validIds = new   ArrayList<>();
    // 8. 双重循环比对,获取用户ID与权限ID的交集  
    for (Long id : activeUserIds) {
        if (roleAuthUserIds.contains(id)) {
            validIds.add(id);
        }
    }
    // 9. 返回有效用户ID列表  
    return  validIds;
}

代码解析: 

该方法需9步完成核心逻辑,步骤繁琐且耦合度高。

.首先通过两次独立if判断实现集合判空,若遗漏任一判空,后续循环会引发NPE。
.筛选启用用户时,需手动创建列表并循环添加,无法直接基于条件过滤,代码冗余。
.去重依赖HashSet特性,需额外创建Set并循环提取ID,增加代码量。
.求交集采用双重循环,roleAuthUserIds.contains(id)会频繁遍历集合,元素多时性能显著下降。
整体代码可维护性差,修改筛选条件或运算逻辑时,需多处调整。

2. CollUtil的判空与默认值能力
集合处理的首要步骤是判空,CollUtil统一了多种集合的判空逻辑,还能提供默认集合避免返回null。
CollUtil的isEmpty方法支持List、Set、Map、Iterator等类型,无需区分集合类型即可判空。
isNotEmpty方法是isEmpty的反向判断,可直接确认集合非空。
defaultIfEmpty在集合为空时返回默认集合,避免调用方因null引发NPE。
emptyIfNull在集合为null时返回不可变空集合,规范返回结果格式。
@Service
public class  UserService {
    // 1. 集合判空与非空校验
    public  void  checkUserList(List<User> userList) {
        // 校验集合是否为空(支持List/Set/Map/Iterator等所有集合类型)  
        if (CollUtil.isEmpty(userList)) {
            throw new   IllegalArgumentException("用户列表不能为空");
        }
        // 校验集合是否非空,等价于!CollUtil.isEmpty(userList),提升可读性  
        if (CollUtil.isNotEmpty(userList)) {
            System.out.println("用户列表共" + userList.size() + "人");
        }
    }

    // 2. 集合为空时返回默认集合
    public  List<User> getDefaultUserList(List<User> userList) {
        // 创建默认用户列表,用于集合为空时返回  
        List<User> defaultUsers = Arrays.asList(new   User(0L, "测试用户", true));
        // 若userList为空(null或size=0),返回defaultUsers;否则返回原集合  
        return  CollUtil.defaultIfEmpty(userList, defaultUsers);
    }

    // 3. 集合为null时返回不可变空集合
    public  List<Long> getEmptySafeUserIds(List<User> userList) {
        // 将null转换为不可变空集合,非null则保持原集合  
        List<User> safeList = CollUtil.emptyIfNull(userList);
        // 提取用户ID,即使集合为空也不会引发NPE  
        return  CollUtil.getFieldValues(safeList, "id", Long.class );
    }
}

代码解析: 

checkUserList方法中,isEmpty统一处理多种集合类型的判空,无需针对List、Set单独编写判空逻辑,减少代码冗余。

isNotEmpty直接替代反向判断,避免!符号导致的可读性问题,代码更直观。
getDefaultUserList方法中,defaultIfEmpty确保调用方始终获取非null集合,即使入参为null或空,也能拿到默认测试用户列表,从源头规避NPE。
getEmptySafeUserIds方法中,emptyIfNull将可能为null的userList转换为不可变空集合,后续getFieldValues提取ID时,无需额外判空即可安全执行。
这些方法规范了集合处理的入口校验,降低了后续业务逻辑的异常风险,同时提升了代码可维护性。

3. CollUtil的去重与集合运算能力
集合去重和运算(交集、并集等)是高频需求,CollUtil封装了这些操作,无需手动循环实现。
CollUtil的distinct方法支持基于元素equals去重,也可按指定字段去重,无需重写自定义对象的equals方法。
intersection方法计算两个集合的交集,自动处理判空,避免双重循环。
union方法实现集合并集,保留所有元素且处理重复次数。
subtract方法计算差集,获取前者有而后者无的元素。
@Service
public class  AuthService {
    // 1. 集合去重(支持简单类型与自定义对象)
    public  List<User> distinctUsers(List<User> userList) {
        // 基于元素equals去重(自定义对象需重写equals和hashCode)  
        List<User> simpleDistinct = CollUtil.distinct(userList);
        // 按用户ID去重,重复时保留第一个元素(无需重写equals方法)  
        return  CollUtil.distinct(userList, User::getId, false);
    }

    // 2. 计算两个集合的交集(获取共同元素)
    public  List<Long> getCommonAuthIds(List<Long> userAuths, List<Long> roleAuths) {
        // 计算交集,自动处理集合判空,返回共同元素列表  
        // 示例:userAuths=[1,2,3,3],roleAuths=[3,4,5],结果为[3]  
        return  CollUtil.intersection(userAuths, roleAuths);
    }

    // 3. 计算并集与差集
    public  List<Long> getUnionAndSubtract(List<Long> userAuths, List<Long> roleAuths) {
        // 计算并集:保留两个集合的所有元素,重复次数取最大值  
        // 示例:userAuths=[1,2,3],roleAuths=[3,4,5],结果为[1,2,3,4,5]  
        List<Long> unionAuths = CollUtil.union(userAuths, roleAuths);
        // 计算差集:获取userAuths中有但roleAuths中没有的元素  
        // 示例:userAuths=[1,2,3],roleAuths=[3,4,5],结果为[1,2]  
        List<Long> subtractAuths = CollUtil.subtract(userAuths, roleAuths);
        // 按业务需求返回差集(可根据场景切换为并集)  
        return  subtractAuths;
    }
}

代码解析: 

distinct方法提供两种去重方式,满足不同场景。

基于元素equals去重时,需确保自定义对象重写equals和hashCode;按指定字段(如User::getId)去重时,无需修改对象结构,直接通过字段值判断重复,灵活性更高。
getCommonAuthIds方法中,intersection自动处理集合判空,无需手动校验userAuths或roleAuths是否为null,避免NPE。
其内部采用更高效的遍历逻辑,性能优于传统双重循环,尤其适合元素较多的集合。
getUnionAndSubtract方法中,union在合并集合时,会保留元素的重复次数(取两个集合中的最大值),满足需保留重复元素的业务场景;
subtract严格遵循“前者有后者无”的逻辑,结果准确且无需手动循环比对。
这些方法将复杂的集合运算封装为单步调用,代码简洁且可维护性强,修改运算逻辑时只需调整方法调用,无需重构循环代码。

4. CollUtil的集合转换与筛选能力
集合转Map、转字符串及筛选元素是查询与展示场景的高频需求,CollUtil简化了这些操作。
fieldValueMap按指定字段生成“字段值-元素”的Map,查询效率从O(n)提升至O(1)。
toListMap在key重复时将value存储为List,避免数据覆盖。
join方法提取字段值并按分隔符拼接,满足前端展示需求。
filter方法支持自定义条件筛选,直接返回符合条件的元素列表。
@Service
public class  UserQueryService {
    // 1. 集合转Map(key为指定字段,value为元素)
    public  Map<Long, User> getUserMapByIds(List<User> userList) {
        // 按用户ID作为key,User对象作为value生成Map  
        // 注意:若ID重复,后续元素会覆盖前面元素(需确保key唯一)  
        return  CollUtil.fieldValueMap(userList, "id");
    }

    // 2. 集合转Map(key重复时value为List)
    public  Map<Long, List<User>> getUserListMapByIds(List<User> userList) {
        // 按用户ID作为key,value为相同ID的User列表,避免重复key导致的数据覆盖  
        return  CollUtil.toListMap(userList, "id");
    }

    // 3. 集合转字符串(用于前端展示)
    public  String getUserNamesStr(List<User> userList) {
        // 提取用户列表中的“name”字段值,生成姓名列表  
        List<String> names = CollUtil.getFieldValues(userList, "name");
        // 按“,”分隔姓名,拼接为字符串(示例结果:“张三,李四”)  
        return  CollUtil.join(names, ",");
    }

    // 4. 按自定义条件筛选元素
    public  List<User> getActiveAdultUsers(List<User> userList) {
        // 筛选非空、启用且年龄>18的用户,返回新集合(原集合不变)  
        return  CollUtil.filter(userList, user -> 
            user != null && user.isActive() && user.getAge() > 18
        );
    }
}

代码解析: 

getUserMapByIds方法中,fieldValueMap无需手动循环put元素,直接通过“id”字段生成Map,后续按ID查询用户时,可通过map.get(1L)快速获取,效率从传统循环的O(n)提升至O(1)。但需注意key唯一性,避免数据覆盖。

getUserListMapByIds方法中,toListMap解决了key重复问题,将相同ID的用户存储为List,适用于一对多场景(如同一部门下的多个用户),避免传统Map因key重复导致的数据丢失。
getUserNamesStr方法中,getFieldValues与join配合使用,先提取姓名字段,再按指定分隔符拼接,无需手动循环拼接字符串,满足“已选用户:张三,李四”这类前端展示需求,代码简洁且易维护。
getActiveAdultUsers方法中,filter支持Lambda表达式作为筛选条件,直接返回符合条件的用户列表,无需手动创建列表并循环添加,逻辑更直观。

同时filter返回新集合,不会修改原集合,避免副作用。这些转换与筛选方法,大幅减少了重复代码,让我们专注于业务逻辑而非基础操作。

5. CollUtil在实际项目开发中的应用
实际项目中,用户权限校验、购物车管理、数据统计、分页展示等场景,均需大量集合处理,CollUtil可显著简化这些场景的代码。
权限校验时,需筛选启用用户、去重并求交集,CollUtil可整合多步操作。
购物车添加商品需避免重复,CollUtil的addIfAbsent方法可直接实现。
数据统计时,CollUtil的countMap可快速生成“元素-次数”的统计Map。
分页展示时,CollUtil的page方法可自动切分集合,无需手动计算索引。
@Service
public class  ProjectApplicationService {
    // 1. 用户权限校验:筛选启用用户ID、去重、求交集
    public  List<Long> getValidUserIds(List<User> userList, List<Long> roleAuthUserIds) {
        // 第一步:筛选非空且启用的用户,排除无效数据  
        List<User> activeUsers = CollUtil.filter(userList, u -> u != null && u.isActive());
        // 第二步:提取用户ID,并基于ID去重(避免重复用户)  
        List<Long> activeUserIds = CollUtil.distinct(CollUtil.getFieldValues(activeUsers, "id", Long.class ));
        // 第三步:求用户ID与角色权限ID的交集,得到最终有效权限ID  
        return  CollUtil.intersection(activeUserIds, roleAuthUserIds);
    }

    // 2. 购物车添加商品:避免重复添加
    public  void  addCartItem(List<CartItem> cart, CartItem item) {
        // 若商品非空且不在购物车中,则添加,返回是否添加成功  
        boolean  isAdded = CollUtil.addIfAbsent(cart, item);
        if (isAdded) {
            System.out.println("商品添加成功");
        } else {
            System.out.println("商品已在购物车中");
        }
    }

    // 3. 商品下单次数统计
    public  Map<String, Integer> countProductOrders(List<String> productIds) {
        // 统计每个商品ID的出现次数,生成“商品ID-下单次数”的Map  
        // 示例:productIds=["P001","P002","P001"],结果为{"P001":2,"P002":1}  
        return  CollUtil.countMap(productIds);
    }

    // 4. 集合分页:按页码和页大小切分数据
    public  List<User> getUsersByPage(List<User> userList, int  pageNo, int  pageSize) {
        // 按页码(从1开始)和页大小切分集合,自动计算起始/结束索引  
        // 示例:pageNo=1,pageSize=10,返回前10条数据;pageNo=2,返回11-20条  
        return  CollUtil.page(pageNo, pageSize, userList);
    }
}

代码解析: 

用户权限校验场景中,通过filter、getFieldValues、distinct、intersection的组合,将传统20多行代码压缩为3行,逻辑清晰且无冗余。筛选、去重、交集运算均通过CollUtil方法实现,无需手动循环,性能更优。购物车添加商品场景中,addIfAbsent整合了“判空+contains判断+添加”的逻辑,无需手动编写if (item != null && !cart.contains(item)),代码更简洁,且避免了contains方法的重复调用。


商品下单次数统计场景中,countMap自动统计元素出现次数,无需创建Map并循环计数,大幅减少代码量。统计结果直接用于展示商品销量排行,无需额外处理。分页场景中,page方法自动计算起始索引((pageNo-1)*pageSize)和结束索引(Math.min(pageNo*pageSize, list.size())),避免手动计算时的索引越界风险。

同时支持从1开始的页码,符合业务习惯。在实际项目中,这些场景覆盖了80%以上的集合处理需求,CollUtil通过封装重复逻辑,让我们专注于业务逻辑,提升了开发效率与代码质量。

结尾
传统集合处理因重复的判空、循环与运算逻辑,导致代码冗余、易出bug且可维护性差。CollUtil通过封装这些基础操作,提供了简洁、高效的API,覆盖判空、去重、运算、转换、筛选、分页等高频场景。合理使用CollUtil,可减少80%以上的重复代码,降低NPE风险,同时提升代码可维护性。

掌握CollUtil不仅能简化当前开发工作,更能规范集合处理逻辑,为后续代码维护提供便利,是Java开发中处理集合的重要工具。
用户评论