巧用 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 集合的内置能力,用最少的代码解决问题,同时兼顾性能。

相关推荐
CoderYanger5 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
CQU_JIAKE7 小时前
11.7[高体】
java·后端·spring
Csvn8 小时前
认识 Nginx
后端·nginx
apcipot_rain8 小时前
C++转Java日记(04)
java·开发语言·c++
红尘散仙8 小时前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
特立独行的猫A9 小时前
我给女儿写了个打字练习:一边敲键盘,一边把小学英语单词记牢
java·后端·python
陈随易9 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
金銀銅鐵10 小时前
同一个 Hello World,Kotlin 和 Java 在字节码层面差在哪
jvm·后端·kotlin
憧憬成为java架构高手的小白10 小时前
LangGraph4j 实战(下)
java
天诰10 小时前
SA-K-11 检索会消失吗与四个未解问题
java·服务器·人工智能·python·机器学习·ai编程