巧用 Set 去重与复合键:高效统计分组内不重复元素的数量

一、问题场景

在列表查询类需求中,经常需要统计每个主单下包含多少种不同的明细项。例如:

  • 每个订单包含多少种不同的商品
  • 每个采购单包含多少种不同的物料
  • 每个用户拥有多少种不同的标签

直接做法是对每个主单单独查询明细并在内存中去重,但当主单数量较多时会产生大量 SQL,性能不佳。本文介绍一种批量查询 + Set 去重 + 复合键分组的高效写法。

二、核心思路

  1. 批量查询:一次 SQL 查出当前页所有主单的明细,避免循环查库
  2. 复合键去重:将判断"是否同种"的多个字段拼接成字符串作为唯一标识
  3. Set 自动去重 :利用 Set 不存储重复元素的特性,size() 即为种类数
  4. Map 分组 :按主单 ID 分组,每个主单对应一个 Set

三、代码实现

1. 批量查询并分组统计
java 复制代码
/**
 * 批量统计当前页每个主单的明细种类数
 *
 * @param records 当前页主单列表
 */
private void fillDetailKindCount(List<OrderListVo> records) {
    Set<Long> orderIds = records.stream()
        .map(OrderListVo::getOrderId)
        .collect(Collectors.toSet());

    // 一次查询所有主单的明细,避免循环查库
    List<OrderItem> items = itemMapper.selectList(
        Wrappers.<OrderItem>lambdaQuery()
            .select(OrderItem::getOrderItemId,
                OrderItem::getOrderId,
                OrderItem::getSource,
                OrderItem::getSourceId,
                OrderItem::getName,
                OrderItem::getSpec,
                OrderItem::getUnit)
            .in(OrderItem::getOrderId, orderIds));

    // Map<主单ID, Set<去重标识>>
    Map<Long, Set<String>> keyMap = new HashMap<>();
    for (OrderItem item : items) {
        String detailKey = buildDetailKey(item);
        keyMap.computeIfAbsent(item.getOrderId(), k -> new HashSet<>()).add(detailKey);
    }

    // 回填种类数
    for (OrderListVo record : records) {
        Set<String> keys = keyMap.get(record.getOrderId());
        record.setDetailKindCount(keys == null ? 0 : keys.size());
    }
}
2. 生成复合去重键
java 复制代码
/**
 * 生成明细种类统计使用的去重标识
 *
 * @param item 明细项
 * @return 去重标识字符串
 */
private String buildDetailKey(OrderItem item) {
    // 自定义明细:按名称、规格、单位拼接判断
    if (SourceEnum.CUSTOM.matches(item.getSource())) {
        return String.join("|", SourceEnum.CUSTOM.getCode(),
            StringUtils.trimToEmpty(item.getName()),
            StringUtils.trimToEmpty(item.getSpec()),
            StringUtils.trimToEmpty(item.getUnit()));
    }
    // 外部系统明细:按来源 + 来源ID判断,更准确
    String sourceKey = item.getSourceId() == null
        ? "ITEM_" + item.getOrderItemId()
        : String.valueOf(item.getSourceId());
    return item.getSource() + "|" + sourceKey;
}

四、关键技巧解析

技巧 1:Set 去重统计种类数

凡是需要"统计不重复元素个数"的场景,优先使用 Set。Set 的 add() 方法自动忽略重复元素,最终 size() 即为去重后的数量,无需手动编写重复判断逻辑。

java 复制代码
Set<String> set = new HashSet<>();
set.add("A");
set.add("A");  // 重复,不会存储
set.add("B");
System.out.println(set.size());  // 输出 2
技巧 2:字符串拼接构建复合键

当一个对象由多个字段共同决定"是否相同"时,可将这些字段用分隔符拼接为字符串作为唯一标识:

  • 无需重写 equals 和 hashCode
  • 无需新建专门的 Key 类
  • 直观易调试

注意 :分隔符必须选用字段值中不会出现的字符,否则会产生歧义。例如字段值若可能包含 |,则应改用 _、# 等其他分隔符。

技巧 3:computeIfAbsent 一行完成分组初始化

Map.computeIfAbsent(key, mappingFunction) 的作用是:若 key 不存在,则通过 mappingFunction 计算一个值并存入,最后返回该值。

java 复制代码
// 传统写法
Set<String> set = map.get(id);
if (set == null) {
    set = new HashSet<>();
    map.put(id, set);
}
set.add(key);

// computeIfAbsent 写法
map.computeIfAbsent(id, k -> new HashSet<>()).add(key);
技巧 4:批量 IN 查询替代循环查询

循环中执行数据库查询是常见的性能问题。假设当前页有 10 条主单,循环查询会产生 10 次 SQL,而批量 IN 查询只需 1 次,显著减少数据库往返开销。

五、方案优势与局限性

维度 说明
性能 批量查询减少 SQL 次数,内存分组效率高
可读性 利用 Set 和 Map 的标准 API,意图清晰
灵活性 去重规则可按数据来源差异化设计
局限性 当单页主单数量极大时,IN 列表过长可能影响 SQL 执行计划,需控制分页大小
适用场景 列表页的关联统计、种类数统计等

六、小结

本文介绍的 Set 去重 + 复合键 + computeIfAbsent 分组是一种通用的统计模式,适用于各类"按主单统计明细种类"的场景。核心在于善用 Java 集合的内置能力,用最少的代码解决问题,同时兼顾性能。

相关推荐
企业数字化笔记18 分钟前
租户停用后数据和任务怎么处理?冻结、恢复与保留策略的实现
android·java·spring boot
CarIise24 分钟前
Spring MVC 请求参数绑定与常用注解
java·spring·mvc
晓得迷路了32 分钟前
栗子前端技术周刊第 150 期 - Stack Overflow 开发者调查结果、TanStack Start 1.0...
javascript·css·后端
vx_Biye_Design35 分钟前
PHP中医养生馆预约系统60841-计算机课程设计、毕业设计
vue.js·spring boot·后端·mysql·php·课程设计·express
IT_陈寒35 分钟前
Vite性能优化完全指南
前端·人工智能·后端
一条泥憨鱼37 分钟前
AI Agent 安全实践:从 MCP 工具调用到 Java 权限校验
java·人工智能·安全
Nebula_g43 分钟前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
萧瑟余晖43 分钟前
Spring Security 6.x新特性与架构演进详解
java·spring·架构
\光辉岁月/1 小时前
1.Object类
java·面向对象·object类
程序员小孔.1 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序