巧用 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 去重统计种类数

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

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

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

  • 无需重写 equalshashCode
  • 无需新建专门的 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 集合的内置能力,用最少的代码解决问题,同时兼顾性能。

相关推荐
步行cgn1 小时前
Spring Environment 详解:Spring Boot 配置管理的核心接口
spring boot·python·spring
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之工程结构与开发环境配置
java·spring·状态模式
FfHUCisI1 小时前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
开发语言·后端·golang
Csvn1 小时前
🐍 Day 12: 编码与字符集 — 告别乱码噩梦
后端·python
lzfshub1 小时前
Open-DIS Python发送DIS实体状态PDU:实现坦克炮塔与主炮部件参数
java·网络·python·dis
魏 无羡1 小时前
springboot 拦截器
java·spring boot·后端
William Dawson2 小时前
Spring Boot 接入华为 MRS Redis 集群(密码认证)全流程实战
spring boot·redis·华为
步行cgn2 小时前
Spring Boot 绑定嵌套 Bean 详解
java·spring boot·后端