Java Stream:Collectors.collectingAndThen() 用法详解

一、基本概念

Collectors.collectingAndThen() 是一个收集器适配器,允许在收集完成后对结果进行转换。

java 复制代码
// 方法签名
public static <T, A, R, RR> Collector<T, A, RR> collectingAndThen(
    Collector<T, A, R> downstream,  // 下游收集器
    Function<R, RR> finisher        // 最终转换函数
)

// 参数说明: 
// T: 输入元素类型 (不变) 
// A: 中间容器类型 (不变) 
// R: 下游收集器的结果类型 
// RR: 最终结果类型(finisher转换后)

其中这里面涉及到的范型参数可以参考下面的解析:

java 复制代码
public interface Collector<T, A, R> {
    // T: 流元素的类型(输入)
    // A: 累加器的中间类型(容器)
    // R: 最终结果的类型(输出)
    
    Supplier<A> supplier();        // 创建容器A
    BiConsumer<A, T> accumulator(); // 将T累加到A
    BinaryOperator<A> combiner();   // 合并两个A(并行流)
    Function<A, R> finisher();     // 将A转换为R
    Set<Characteristics> characteristics();
}

示例分析:

java 复制代码
List<String> list = Arrays.asList("a", "b", "c");

List<String> result = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),        // Collector<String, List<String>, List<String>>
        Collections::unmodifiableList // Function<List<String>, List<String>>
    ));
    
// 类型推导:
// Collector<T=String, A=List<String>, R=List<String>>
// Function<R=List<String>, RR=List<String>>
// 最终 Collector<T=String, A=List<String>, RR=List<String>>

二、基本用法

示例1:收集为不可变集合

java 复制代码
List<String> list = Arrays.asList("a", "b", "c");

// 转换为不可修改的List
List<String> unmodifiableList = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),       // 先收集为List
        Collections::unmodifiableList  // 再转换为不可修改
    ));

// 尝试修改会抛出异常
// unmodifiableList.add("d"); // UnsupportedOperationException

示例2:获取集合大小

java 复制代码
List<String> list = Arrays.asList("a", "b", "c", "d");

// 直接计算集合大小
int size = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),  // 收集为List
        List::size            // 转换为大小
    ));

System.out.println(size); // 输出: 4

示例3:分组后执行操作

java 复制代码
List<Person> people = Arrays.asList(
    new Person("Alice", "New York"),
    new Person("Bob", "New York"),
    new Person("Charlie", "London")
);

// 按城市分组,并获取每个组的数量
Map<String, Integer> cityCount = people.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Person::getCity),
        map -> {
            // 对分组结果进行处理
            Map<String, Integer> result = new HashMap<>();
            map.forEach((city, list) -> result.put(city, list.size()));
            return result;
        }
    ));

示例4:获取最大值或最小值

java 复制代码
List<Integer> numbers = Arrays.asList(1, 3, 5, 2, 4);

// 获取最大值,如果没有则返回默认值
Integer max = numbers.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.maxBy(Integer::compareTo),
        opt -> opt.orElse(-1)  // 处理Optional
    ));

System.out.println(max); // 输出: 5

示例5:连接字符串并添加前缀后缀

java 复制代码
List<String> words = Arrays.asList("Hello", "World", "Java");

// 用逗号连接,并添加方括号
String result = words.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.joining(", "),  // 先连接
        s -> "[" + s + "]"         // 再加括号
    ));

System.out.println(result); // 输出: [Hello, World, Java]

示例6:收集为Set并排序

java 复制代码
List<Integer> numbers = Arrays.asList(5, 2, 5, 1, 3, 2);

// 去重后排序
List<Integer> sortedUnique = numbers.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(TreeSet::new),  // 自动去重和排序
        ArrayList::new  // 转换为List
    ));

System.out.println(sortedUnique); // 输出: [1, 2, 3, 5]

示例7:多重转换

java 复制代码
List<Employee> employees = Arrays.asList(
    new Employee("Alice", 5000),
    new Employee("Bob", 6000),
    new Employee("Charlie", 5000)
);

// 按薪资分组 -> 获取名字列表 -> 排序
Map<Integer, List<String>> salaryToNames = employees.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(
            Employee::getSalary,
            Collectors.mapping(Employee::getName, Collectors.toList())
        ),
        map -> {
            // 对每个分组的名字列表排序
            map.replaceAll((k, v) -> {
                Collections.sort(v);
                return v;
            });
            return map;
        }
    ));

示例8:数据库查询结果处理

java 复制代码
// 查询用户列表,按部门分组,统计每个部门的平均工资
List<User> users = userRepository.findAll();

Map<String, Double> deptAvgSalary = users.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(
            User::getDepartment,
            Collectors.averagingDouble(User::getSalary)
        ),
        map -> {
            // 保留两位小数
            Map<String, Double> result = new LinkedHashMap<>();
            map.forEach((dept, avg) -> 
                result.put(dept, Math.round(avg * 100.0) / 100.0));
            return result;
        }
    ));

示例9:订单统计

java 复制代码
List<Order> orders = orderService.getRecentOrders();

// 统计每个用户的订单总金额,按金额降序排序
List<UserTotal> userTotals = orders.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(
            Order::getUserId,
            Collectors.summingDouble(Order::getAmount)
        ),
        map -> map.entrySet().stream()
            .map(e -> new UserTotal(e.getKey(), e.getValue()))
            .sorted(Comparator.comparing(UserTotal::getTotal).reversed())
            .collect(Collectors.toList())
    ));

三、使用建议

"用 collectingAndThen 只做一件事,复杂的就分步,简单的直接做,保持代码干净。"

三个核心原则:

  1. 只做简单转换
  2. 一次只做一件事
  3. 数据量大才考虑使用

1. 简单原则

能用简单方式就不用复杂收集器

java 复制代码
// ❌ 过度使用
List<String> result = list.stream()
    .collect(collectingAndThen(
        toList(),
        Collections::unmodifiableList
    ));

// ✅ 简单方式(Java 10+)
List<String> result = List.copyOf(list);
// 或
List<String> result = Collections.unmodifiableList(new ArrayList<>(list));

2. 保持函数纯净

不要在转换函数中有副作用

java 复制代码
// ❌ 有副作用
Map<String, List<Person>> result = persons.stream()
    .collect(collectingAndThen(
        groupingBy(Person::getCity),
        map -> {
            map.forEach((k, v) -> System.out.println(k)); // 副作用
            return map;
        }
    ));

// ✅ 纯函数
Map<String, List<Person>> result = persons.stream()
    .collect(groupingBy(Person::getCity));
// 副作用放到后面
result.forEach((k, v) -> System.out.println(k));

3. 性能意识

小数据不要用,大数据才考虑

java 复制代码
List<String> smallList = Arrays.asList("a", "b", "c"); // 只有3个元素

// ❌ 没必要用 collectingAndThen
List<String> result1 = smallList.stream()
    .collect(collectingAndThen(toList(), Collections::unmodifiableList));

// ✅ 直接创建不可变集合
List<String> result2 = List.copyOf(smallList); // Java 10+
// 或
List<String> result3 = Collections.unmodifiableList(new ArrayList<>(smallList));

// ✅ 大数据集用 collectingAndThen
List<String> largeList = getLargeList(); // 假设有上万条
List<String> result = largeList.stream()
    .collect(collectingAndThen(toList(), Collections::unmodifiableList));

4. 常用场景才用

不要为了用而用

java 复制代码
// ✅ 真正有用的场景:

// 1. 收集后立即转换
List<String> result = list.stream()
    .collect(collectingAndThen(
        toList(),
        Collections::unmodifiableList  // 立即转为不可变
    ));

// 2. 处理 Optional 结果
String max = list.stream()
    .collect(collectingAndThen(
        maxBy(String::compareTo),
        opt -> opt.orElse("default")  // 解包Optional
    ));

// 3. 连接字符串后包装
String joined = list.stream()
    .collect(collectingAndThen(
        joining(", "),
        s -> "[" + s + "]"  // 添加括号
    ));

// ❌ 没必要用的场景:
// - 只是简单的收集
// - 后续还有更多处理步骤
// - 转换逻辑很复杂

5. 复杂逻辑就分步

不要都塞进 collectingAndThen

java 复制代码
// ❌ 过于复杂
List<String> result = list.stream()
    .collect(collectingAndThen(
        toList(),
        list -> {
            // 一大堆复杂逻辑
            list.sort(Comparator.reverseOrder());
            list.replaceAll(String::toUpperCase);
            return Collections.unmodifiableList(list);
        }
    ));

// ✅ 分步处理更清晰
// 步骤1:收集和排序
List<String> temp = list.stream()
    .sorted(Comparator.reverseOrder())
    .collect(toList());

// 步骤2:转换
temp.replaceAll(String::toUpperCase);

// 步骤3:转为不可变
List<String> result = Collections.unmodifiableList(temp);
相关推荐
摸鱼的春哥2 小时前
企业自建低代码平台正在扼杀AI编程的生长
前端·javascript·后端
程序员爱钓鱼2 小时前
Node.js 编程实战:博客系统 —— 数据库设计
前端·后端·node.js
Logan Lie2 小时前
Go 反射(Reflection)详解:从入门到实践
开发语言·后端·golang
superman超哥3 小时前
Rust 异步性能的黑盒与透视:Tokio 监控与调优实战
开发语言·后端·rust·编程语言·rust异步性能·rust黑盒与透视·tokio监控与调优
Mr -老鬼3 小时前
Rust 知识图谱 -进阶部分
开发语言·后端·rust
guchen663 小时前
CircularBuffer 优化历程:从数组越界到线程安全的完美实现
后端
古城小栈3 小时前
Cargo.toml
开发语言·后端·rust
悟空码字3 小时前
10分钟搞定!SpringBoot集成腾讯云短信全攻略,从配置到发送一气呵成
java·spring boot·后端
星浩AI3 小时前
从0到1:用LlamaIndex工作流构建Text-to-SQL应用完整指南
人工智能·后端·python