Hutool CollStreamUtil 一行搞定分组、转换、合并全场景

Stream API 虽然强大,但复杂场景下代码依然繁琐

  • 想按两个字段(如年级 + 班级)分组,要写嵌套的groupingBy,代码嵌套深、可读性差;

  • 把集合转 Map/List/Set,要先写stream()collect(),重复代码多;

  • 合并两个 Map,要手动遍历处理重复 key,逻辑复杂;

  • 分组后只保留某个字段的列表,还要额外写mappingtoList()

其实 Hutool 的CollStreamUtil早就把这些操作封装好了一行代码实现双层分组、集合转换、Map 合并,基于 Stream API 但比原生更简洁,让集合处理效率翻倍!

关键说明

  • 核心优势:基于 Stream API,比原生更简洁;支持双层分组、一键集合转换、Map 合并;支持并行处理(isParallel参数);

  • 适用场景:集合分组、转换、合并等高频操作;

  • 设计理念:简化 Stream API 的繁琐调用,将常用集合操作封装为一行代码。

CollStreamUtil 核心优势

  1. API更简洁:无需手动调用stream()和collect(),直接传入集合和函数式接口,一行搞定;
  2. 支持双层分组:原生 Stream 需要嵌套groupingBy,CollStreamUtil 直接支持按两个字段分组;
  3. 集合转换便捷:一键实现Collection→List/Set/Map,支持泛型转换;
  4. Map合并简单:一行代码合并两个 Map,支持自定义合并逻辑;
  5. 支持并行处理:所有方法都有isParallel参数,大数据量场景可开启并行提升性能;
  6. 兼容原生Stream:底层基于 Stream API,支持所有 Stream 的函数式接口。

简单说,CollStreamUtil 就是 “Stream API 的简化版”,让集合处理从 “多行嵌套” 变成 “一行搞定”。

按场景分类,直接复制能用

场景 1:分组操作(单层分组、双层分组,最常用)

1.1 单层分组:按单个字段分组(groupByKey)

将集合按指定字段分组,返回Map<K, List<E>>

import cn.hutool.core.collection.CollStreamUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class CollStreamGroupDemo {
    // 测试实体类:学生
    static class Student {
        private String name;
        private int grade; // 年级
        private int clazz; // 班级
        private int score; // 分数

        public Student(String name, int grade, int clazz, int score) {
            this.name = name;
            this.grade = grade;
            this.clazz = clazz;
            this.score = score;
        }

        // getter/setter省略
        public String getName() { return name; }
        public int getGrade() { return grade; }
        public int getClazz() { return clazz; }
        public int getScore() { return score; }

        @Override
        public String toString() {
            return name + "(年级" + grade + "班" + clazz + ",分数" + score + ")";
        }
    }

    public static void main(String[] args) {
        // 准备测试数据
        List<Student> students = Arrays.asList(
                new Student("张三", 1, 1, 95),
                new Student("李四", 1, 2, 88),
                new Student("王五", 1, 1, 92),
                new Student("赵六", 2, 1, 85),
                new Student("孙七", 2, 2, 90),
                new Student("周八", 2, 1, 87)
        );

        // 1. 按年级分组(单层分组)
        Map<Integer, List<Student>> gradeGroup = CollStreamUtil.groupByKey(students, Student::getGrade);
        System.out.println("按年级分组:");
        gradeGroup.forEach((grade, studentList) -> {
            System.out.println("  年级" + grade + ":" + studentList);
        });

        // 2. 按年级分组,只保留姓名列表(groupKeyValue)
        Map<Integer, List<String>> gradeNameGroup = CollStreamUtil.groupKeyValue(
                students,
                Student::getGrade,  // 分组key:年级
                Student::getName     // 分组value:姓名
        );
        System.out.println("\n按年级分组,只保留姓名:");
        gradeNameGroup.forEach((grade, names) -> {
            System.out.println("  年级" + grade + ":" + names);
        });
    }
}
1.2 双层分组:按两个字段分组(groupBy2Key、group2Map)

原生 Stream 需要嵌套groupingBy,CollStreamUtil 直接支持按两个字段分组:

// 接上面的代码
public static void main(String[] args) {
    // 准备测试数据...(同上)

    // 3. 按年级+班级分组(双层分组,value是学生列表)
    Map<Integer, Map<Integer, List<Student>>> gradeClazzGroup = CollStreamUtil.groupBy2Key(
            students,
            Student::getGrade,  // 第一层key:年级
            Student::getClazz   // 第二层key:班级
    );
    System.out.println("\n按年级+班级分组(学生列表):");
    gradeClazzGroup.forEach((grade, clazzMap) -> {
        System.out.println("  年级" + grade + ":");
        clazzMap.forEach((clazz, studentList) -> {
            System.out.println("    班级" + clazz + ":" + studentList);
        });
    });

    // 4. 按年级+班级分组(双层分组,value是单个学生,需确保key唯一)
    // 注意:如果同一年级+班级有多个学生,会覆盖前面的学生(类似toMap的重复key处理)
    Map<Integer, Map<Integer, Student>> gradeClazzSingleGroup = CollStreamUtil.group2Map(
            students,
            Student::getGrade,  // 第一层key:年级
            Student::getClazz   // 第二层key:班级
    );
    System.out.println("\n按年级+班级分组(单个学生):");
    gradeClazzSingleGroup.forEach((grade, clazzMap) -> {
        System.out.println("  年级" + grade + ":");
        clazzMap.forEach((clazz, student) -> {
            System.out.println("    班级" + clazz + ":" + student);
        });
    });
}
1.3 通用分组:自定义下游 Collector(groupBy)

支持自定义下游 Collector,功能更灵活(类似原生Collectors.groupingBy):

// 接上面的代码
import java.util.stream.Collectors;

public static void main(String[] args) {
    // 准备测试数据...(同上)

    // 5. 按年级分组,统计每组平均分(使用自定义下游Collector)
    Map<Integer, Double> gradeAvgScore = CollStreamUtil.groupBy(
            students,
            Student::getGrade,  // 分组key:年级
            Collectors.averagingInt(Student::getScore)  // 下游Collector:统计平均分
    );
    System.out.println("\n按年级分组,统计平均分:");
    gradeAvgScore.forEach((grade, avgScore) -> {
        System.out.println("  年级" + grade + ":平均分" + avgScore);
    });
}

场景 2:集合转换(List/Set/Map,一键转换)

无需手动调用stream()collect(),直接转换集合类型:

import cn.hutool.core.collection.CollStreamUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CollStreamConvertDemo {
    // 测试实体类:商品
    static class Product {
        private Long id;
        private String name;
        private Double price;

        public Product(Long id, String name, Double price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }

        // getter/setter省略
        public Long getId() { return id; }
        public String getName() { return name; }
        public Double getPrice() { return price; }
    }

    public static void main(String[] args) {
        // 准备测试数据
        List<Product> products = Arrays.asList(
                new Product(1L, "手机", 2999.0),
                new Product(2L, "电脑", 5999.0),
                new Product(3L, "平板", 3999.0),
                new Product(4L, "耳机", 299.0)
        );

        // 1. Collection→List:提取商品名称列表
        List<String> productNames = CollStreamUtil.toList(products, Product::getName);
        System.out.println("商品名称列表:" + productNames);
        // 输出:[手机, 电脑, 平板, 耳机]

        // 2. Collection→Set:提取商品价格Set(自动去重)
        Set<Double> productPrices = CollStreamUtil.toSet(products, Product::getPrice);
        System.out.println("商品价格Set:" + productPrices);
        // 输出:[299.0, 2999.0, 3999.0, 5999.0]

        // 3. Collection→Map:id→Product对象(toIdentityMap)
        Map<Long, Product> productMap = CollStreamUtil.toIdentityMap(products, Product::getId);
        System.out.println("id→Product Map:" + productMap);
        // 输出:{1=Product{id=1, name='手机', price=2999.0}, 2=Product{id=2, name='电脑', price=5999.0}, ...}

        // 4. Collection→Map:id→name(toMap,value类型与原集合不同)
        Map<Long, String> idNameMap = CollStreamUtil.toMap(products, Product::getId, Product::getName);
        System.out.println("id→name Map:" + idNameMap);
        // 输出:{1=手机, 2=电脑, 3=平板, 4=耳机}
    }
}

场景 3:Map 合并(merge,支持自定义合并逻辑)

一行代码合并两个 Map,支持自定义重复 key 的合并逻辑:

import cn.hutool.core.collection.CollStreamUtil;
import java.util.HashMap;
import java.util.Map;

public class CollStreamMergeDemo {
    public static void main(String[] args) {
        // 准备两个Map(比如:电商订单的商品销量)
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("手机", 100);
        map1.put("电脑", 50);
        map1.put("平板", 30);

        Map<String, Integer> map2 = new HashMap<>();
        map2.put("手机", 200);
        map2.put("耳机", 150);
        map2.put("平板", 70);

        // 1. 合并两个Map,重复key的value累加(比如:合并两个渠道的销量)
        Map<String, Integer> mergedMap = CollStreamUtil.merge(
                map1,
                map2,
                (v1, v2) -> v1 + v2  // 合并逻辑:销量累加
        );
        System.out.println("合并后的销量Map(累加):" + mergedMap);
        // 输出:{手机=300, 电脑=50, 平板=100, 耳机=150}

        // 2. 合并两个Map,重复key保留最大值(比如:保留最高销量)
        Map<String, Integer> maxMap = CollStreamUtil.merge(
                map1,
                map2,
                Math::max  // 合并逻辑:取最大值
        );
        System.out.println("合并后的销量Map(最大值):" + maxMap);
        // 输出:{手机=200, 电脑=50, 平板=70, 耳机=150}

        // 3. 合并两个Map,重复key拼接字符串(比如:合并商品的标签)
        Map<String, String> tagMap1 = new HashMap<>();
        tagMap1.put("手机", "智能");
        tagMap1.put("电脑", "轻薄");

        Map<String, String> tagMap2 = new HashMap<>();
        tagMap2.put("手机", "大屏");
        tagMap2.put("平板", "便携");

        Map<String, String> mergedTagMap = CollStreamUtil.merge(
                tagMap1,
                tagMap2,
                (v1, v2) -> v1 + "," + v2  // 合并逻辑:标签拼接
        );
        System.out.println("合并后的标签Map(拼接):" + mergedTagMap);
        // 输出:{手机=智能,大屏, 电脑=轻薄, 平板=便携}
    }
}

基于 CollStreamUtil 实现电商订单统计

在实际项目中,电商订单统计是常见需求,用 CollStreamUtil 可大幅简化代码:

需求:

  • 按用户 ID + 订单状态分组,统计每个用户不同状态的订单数量;

  • 提取所有已完成订单的商品 ID 列表;

  • 合并两个渠道的订单销量数据。

实现:

import cn.hutool.core.collection.CollStreamUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class OrderStatisticsDemo {
    // 订单实体类
    static class Order {
        private Long orderId;
        private Long userId;
        private Integer status; // 1-待支付,2-已支付,3-已完成
        private List<Long> productIds; // 订单包含的商品ID列表

        public Order(Long orderId, Long userId, Integer status, List<Long> productIds) {
            this.orderId = orderId;
            this.userId = userId;
            this.status = status;
            this.productIds = productIds;
        }

        // getter/setter省略
        public Long getOrderId() { return orderId; }
        public Long getUserId() { return userId; }
        public Integer getStatus() { return status; }
        public List<Long> getProductIds() { return productIds; }
    }

    public static void main(String[] args) {
        // 准备测试订单数据
        List<Order> orders = Arrays.asList(
                new Order(1L, 1001L, 3, Arrays.asList(101L, 102L)),
                new Order(2L, 1001L, 2, Arrays.asList(103L)),
                new Order(3L, 1002L, 3, Arrays.asList(101L, 104L)),
                new Order(4L, 1002L, 3, Arrays.asList(102L)),
                new Order(5L, 1003L, 1, Arrays.asList(105L))
        );

        System.out.println("=== 电商订单统计 ===");

        // 1. 按用户ID+订单状态分组,统计订单数量
        Map<Long, Map<Integer, Long>> userStatusOrderCount = CollStreamUtil.groupBy(
                orders,
                Order::getUserId,  // 第一层key:用户ID
                CollStreamUtil.groupBy(
                        Order::getStatus,  // 第二层key:订单状态
                        CollStreamUtil.toMap(
                                Order::getStatus,  // 内层key:订单状态
                                order -> 1L,  // 初始数量为1
                                Long::sum  // 重复key累加
                        )
                )
        );
        System.out.println("1. 用户ID+订单状态的订单数量:");
        userStatusOrderCount.forEach((userId, statusMap) -> {
            System.out.println("   用户" + userId + ":");
            statusMap.forEach((status, count) -> {
                String statusName = getStatusName(status);
                System.out.println("     " + statusName + ":" + count + "个订单");
            });
        });

        // 2. 提取所有已完成订单的商品ID列表(去重)
        Set<Long> completedProductIds = CollStreamUtil.toSet(
                orders.stream()
                        .filter(order -> order.getStatus() == 3)  // 只保留已完成订单
                        .flatMap(order -> order.getProductIds().stream())  // 扁平化为商品ID流
                        .toList(),
                id -> id  // 直接返回ID(toSet自动去重)
        );
        System.out.println("\n2. 已完成订单的商品ID列表:" + completedProductIds);

        // 3. 合并两个渠道的订单销量数据
        Map<Long, Integer> channel1Sales = new HashMap<>();
        channel1Sales.put(101L, 100);
        channel1Sales.put(102L, 80);

        Map<Long, Integer> channel2Sales = new HashMap<>();
        channel2Sales.put(101L, 150);
        channel2Sales.put(103L, 50);

        Map<Long, Integer> totalSales = CollStreamUtil.merge(
                channel1Sales,
                channel2Sales,
                Integer::sum  // 销量累加
        );
        System.out.println("\n3. 合并后的商品销量:" + totalSales);
    }

    // 获取订单状态名称
    private static String getStatusName(Integer status) {
        switch (status) {
            case 1: return "待支付";
            case 2: return "已支付";
            case 3: return "已完成";
            default: return "未知状态";
        }
    }
}

运行效果:

  • 按用户 ID + 订单状态分组,清晰展示每个用户不同状态的订单数量;

  • 提取已完成订单的商品 ID,自动去重;

  • 合并两个渠道的销量数据,销量累加;

  • 代码简洁,逻辑清晰,比原生 Stream API 少写一半代码。

避坑指南

1. 并行处理的使用场景

  • 所有方法都有isParallel参数,设置为true时使用并行 Stream;

  • 适用场景:大数据量(万级以上)、CPU 密集型操作;

  • 注意事项:并行 Stream 会增加线程开销,小数据量场景性能可能反而下降;并行处理时需确保函数式接口是线程安全的。

2. 双层分组的 key 唯一性

    • group2Map方法:第二层 Map 的 value 是单个元素,需确保同一key1+key2组合唯一,否则会覆盖前面的元素;

    • 如果存在重复 key,建议使用groupBy2Key(value 是 List),或结合groupBy自定义下游 Collector 处理。

    3. Map 合并的 null 值处理

      • merge方法:如果两个 Map 中存在 null value,合并逻辑需处理 null,否则会抛出NullPointerException;

      • 示例:(v1, v2) -> (v1 == null ? 0 : v1) + (v2 == null ? 0 : v2)(数值类型)。

      总结

      CollStreamUtil 的核心价值就是 “简化 Stream API 的繁琐调用,让集合处理更高效”—— 它将常用的集合分组、转换、合并操作封装为一行代码,比原生 Stream API 更简洁、更易读。

      它的用法可以总结为 3 大核心场景:

        • 分组操作:单层分组(groupByKey)、双层分组(groupBy2Key、group2Map)、通用分组(groupBy);

        • 集合转换:Collection→List(toList)、Collection→Set(toSet)、Collection→Map(toMap/toIdentityMap);

        • Map 合并:merge方法,支持自定义合并逻辑。

        如果你经常使用 Stream API 处理集合,赶紧试试 CollStreamUtil—— 它能帮你避免嵌套 Stream 的繁琐代码,让集合处理变得简单高效。搭配 Hutool 的ListUtilMapUtil等工具类,还能实现更复杂的数据处理场景,效率翻倍!

        使用 Hutool 的 ExcelUtil 工具类,可以实现将某个 Excel 表格中一行每两列进行合并的操作。 具体实现步骤如下: 1. 读取 Excel 文件,获取到需要操作的 Sheet。 ```java ExcelReader reader = ExcelUtil.getReader("file path"); Sheet sheet = reader.getSheet(sheetIndex); ``` 2. 遍历需要合并的行,对于每一行,获取到需要合并的单元格开始和结束的列号,然后调用 Hutool 提供的合并单元格方法进行合并。 ```java // 遍历需要合并的行 for (int i = startRow; i <= endRow; i++) { // 记录当前需要合并的单元格范围的起始和结束列号 int startCol = -1; int endCol = -1; // 遍历当前行的所有单元格,找到需要合并的单元格范围 for (int j = 0; j < sheet.getRow(i).getLastCellNum(); j += 2) { Cell cell = sheet.getRow(i).getCell(j); if (cell == null || StringUtils.isBlank(cell.getStringCellValue())) { continue; } if (startCol == -1) { startCol = j; } else { endCol = j; // 合并单元格 sheet.addMergedRegion(new CellRangeAddress(i, i, startCol, endCol)); startCol = -1; endCol = -1; } } // 如果最后还有未合并的单元格,需要特殊处理 if (startCol != -1 && endCol == -1) { endCol = sheet.getRow(i).getLastCellNum() - 1; sheet.addMergedRegion(new CellRangeAddress(i, i, startCol, endCol)); } } ``` 3. 将操作后的 Excel 文件输出到指定路径。 ```java ExcelWriter writer = ExcelUtil.getWriter("output file path"); writer.setSheet(sheetIndex); writer.write(sheet); writer.flush(); writer.close(); ``` 完整代码示例: ```java import cn.hutool.core.util.StrUtil; import cn.hutool.poi.excel.ExcelReader; import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.ExcelWriter; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; import java.io.File; import java.io.IOException; public class MergeColumns { public static void main(String[] args) throws IOException { // 读取 Excel 文件 ExcelReader reader = ExcelUtil.getReader(new File("test.xlsx")); Sheet sheet = reader.getSheet(0); // 遍历需要合并的行 for (int i = 0; i <= sheet.getLastRowNum(); i++) { // 记录当前需要合并的单元格范围的起始和结束列号 int startCol = -1; int endCol = -1; // 遍历当前行的所有单元格,找到需要合并的单元格范围 for (int j = 0; j < sheet.getRow(i).getLastCellNum(); j += 2) { Cell cell = sheet.getRow(i).getCell(j); if (cell == null || StrUtil.isBlank(cell.getStringCellValue())) { continue; } if (startCol == -1) { startCol = j; } else { endCol = j; // 合并单元格 sheet.addMergedRegion(new CellRangeAddress(i, i, startCol, endCol)); startCol = -1; endCol = -1; } } // 如果最后还有未合并的单元格,需要特殊处理 if (startCol != -1 && endCol == -1) { endCol = sheet.getRow(i).getLastCellNum() - 1; sheet.addMergedRegion(new CellRangeAddress(i, i, startCol, endCol)); } } // 输出到指定路径 ExcelWriter writer = ExcelUtil.getWriter(new File("output.xlsx")); writer.setSheet(0); writer.write(sheet); writer.flush(); writer.close(); } } ```
        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

        1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
        2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

        余额充值