跳到主要内容

Java Stream收集

Stream 收集操作是 Java Stream API 中最重要的终端操作之一,它允许我们将流处理的结果收集到不同类型的容器中,或者执行各种汇总、分组等操作。本文将详细介绍 Stream 收集的概念和使用方法。

什么是 Stream 收集?

Stream 收集指的是将流中的元素聚合到一个结果容器中的过程。Java Stream API 提供了 collect() 方法和 Collectors 工具类,使我们能够轻松地将 Stream 转换为列表、集合、映射等,或执行各种数据统计和聚合操作。

收集器 (Collector)

收集器是 Stream 收集操作的核心,它定义了如何收集流中的元素。Java 提供了 java.util.stream.Collectors 类,其中包含了各种预定义的收集器。

基本收集操作

收集到集合

将流元素收集到 List、Set 或其他集合类型是最常见的收集操作。

收集到 List

java
List<String> list = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.toList());
System.out.println(list); // 输出: [苹果, 香蕉, 橙子]

收集到 Set

java
Set<String> set = Stream.of("苹果", "香蕉", "苹果", "橙子")
.collect(Collectors.toSet());
System.out.println(set); // 输出: [苹果, 橙子, 香蕉](顺序可能不同)

收集到特定集合类型

java
LinkedList<String> linkedList = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.toCollection(LinkedList::new));
System.out.println(linkedList); // 输出: [苹果, 香蕉, 橙子]

收集到字符串

将流元素连接成一个字符串:

java
String joined = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.joining(", "));
System.out.println(joined); // 输出: 苹果, 香蕉, 橙子

带前缀和后缀:

java
String joinedWithPrefixSuffix = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.joining(", ", "水果: [", "]"));
System.out.println(joinedWithPrefixSuffix); // 输出: 水果: [苹果, 香蕉, 橙子]

高级收集操作

收集到 Map

基本用法

java
Map<String, Integer> lengthMap = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.toMap(
s -> s, // 键映射函数
s -> s.length() // 值映射函数
));
System.out.println(lengthMap); // 输出: {苹果=2, 橙子=2, 香蕉=2}

处理重复键

当流中可能存在重复键时,需要提供一个合并函数:

java
Map<Integer, String> lengthToFruit = Stream.of("苹果", "香蕉", "橙子", "梨")
.collect(Collectors.toMap(
String::length, // 键映射函数 (长度)
s -> s, // 值映射函数 (水果名)
(existing, replacement) -> existing + ", " + replacement // 合并函数
));
System.out.println(lengthToFruit); // 输出: {2=苹果, 橙子, 梨, 香蕉}

指定 Map 实现类型

java
TreeMap<String, Integer> sortedMap = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.toMap(
s -> s,
String::length,
(e1, e2) -> e1,
TreeMap::new
));
System.out.println(sortedMap); // 输出: {橙子=2, 苹果=2, 香蕉=2}(按键排序)

分组和分区

分组 (groupingBy)

按照特定条件将流元素分组:

java
List<String> fruits = Arrays.asList("苹果", "香蕉", "橙子", "梨", "葡萄");
Map<Integer, List<String>> groupByLength = fruits.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(groupByLength); // 输出: {1=[梨], 2=[苹果, 橙子, 葡萄], 2=[香蕉]}

多级分组

java
Map<Integer, Map<Character, List<String>>> groupByLengthAndFirstChar = fruits.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.groupingBy(s -> s.charAt(0))
));
System.out.println(groupByLengthAndFirstChar);
// 输出类似: {1={梨=[梨]}, 2={苹=[苹果], 橙=[橙子], 葡=[葡萄]}, 2={香=[香蕉]}}

分区 (partitioningBy)

按照条件将流元素分为两组(true 和 false):

java
Map<Boolean, List<String>> partitioned = fruits.stream()
.collect(Collectors.partitioningBy(s -> s.length() > 1));
System.out.println(partitioned);
// 输出: {false=[梨], true=[苹果, 香蕉, 橙子, 葡萄]}

聚合操作

计数

java
long count = Stream.of("苹果", "香蕉", "橙子")
.collect(Collectors.counting());
System.out.println(count); // 输出: 3

求和

java
List<Product> products = Arrays.asList(
new Product("苹果", 5.0),
new Product("香蕉", 2.0),
new Product("橙子", 3.0)
);

double totalPrice = products.stream()
.collect(Collectors.summingDouble(Product::getPrice));
System.out.println(totalPrice); // 输出: 10.0

平均值

java
double averagePrice = products.stream()
.collect(Collectors.averagingDouble(Product::getPrice));
System.out.println(averagePrice); // 输出: 3.3333333333333335

最大值和最小值

java
Optional<Product> mostExpensive = products.stream()
.collect(Collectors.maxBy(Comparator.comparing(Product::getPrice)));
mostExpensive.ifPresent(p -> System.out.println("最贵的产品: " + p.getName())); // 输出: 最贵的产品: 苹果

统计汇总

java
DoubleSummaryStatistics stats = products.stream()
.collect(Collectors.summarizingDouble(Product::getPrice));
System.out.println(stats);
// 输出类似: DoubleSummaryStatistics{count=3, sum=10.000000, min=2.000000, average=3.333333, max=5.000000}

自定义收集器

除了使用 Collectors 提供的预定义收集器外,我们还可以创建自定义收集器。

java
Collector<String, StringBuilder, String> customCollector = Collector.of(
StringBuilder::new, // 提供者函数
(builder, str) -> builder.append(str), // 累加器函数
(b1, b2) -> b1.append(b2), // 组合器函数
StringBuilder::toString // 最终转换函数
);

String result = Stream.of("苹果", "香蕉", "橙子")
.collect(customCollector);
System.out.println(result); // 输出: 苹果香蕉橙子

实际案例:销售数据分析

假设我们有一组销售订单数据,需要进行各种统计和分析:

java
class Order {
private String product;
private double amount;
private String category;

public Order(String product, double amount, String category) {
this.product = product;
this.amount = amount;
this.category = category;
}

public String getProduct() { return product; }
public double getAmount() { return amount; }
public String getCategory() { return category; }
}

// 创建测试数据
List<Order> orders = Arrays.asList(
new Order("笔记本电脑", 6999.0, "电子产品"),
new Order("手机", 3999.0, "电子产品"),
new Order("T恤", 99.0, "服装"),
new Order("牛仔裤", 199.0, "服装"),
new Order("咖啡机", 1299.0, "家电"),
new Order("微波炉", 599.0, "家电")
);

按类别分组并计算总金额

java
Map<String, Double> totalByCategory = orders.stream()
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.summingDouble(Order::getAmount)
));

System.out.println("各类别总销售额:");
totalByCategory.forEach((category, total) ->
System.out.println(category + ": " + total));

// 输出:
// 各类别总销售额:
// 电子产品: 10998.0
// 服装: 298.0
// 家电: 1898.0

查找每个类别中最贵的产品

java
Map<String, Optional<Order>> mostExpensiveByCategory = orders.stream()
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.maxBy(Comparator.comparing(Order::getAmount))
));

System.out.println("\n各类别最贵产品:");
mostExpensiveByCategory.forEach((category, order) ->
order.ifPresent(o -> System.out.println(category + ": " + o.getProduct() + " - " + o.getAmount())));

// 输出:
// 各类别最贵产品:
// 电子产品: 笔记本电脑 - 6999.0
// 服装: 牛仔裤 - 199.0
// 家电: 咖啡机 - 1299.0

统计各类别产品数量和价格统计信息

java
Map<String, DoubleSummaryStatistics> statisticsByCategory = orders.stream()
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.summarizingDouble(Order::getAmount)
));

System.out.println("\n各类别统计信息:");
statisticsByCategory.forEach((category, stats) -> {
System.out.println(category + ":");
System.out.println(" 产品数量: " + stats.getCount());
System.out.println(" 总金额: " + stats.getSum());
System.out.println(" 平均价格: " + stats.getAverage());
System.out.println(" 最低价格: " + stats.getMin());
System.out.println(" 最高价格: " + stats.getMax());
});

// 输出:
// 各类别统计信息:
// 电子产品:
// 产品数量: 2
// 总金额: 10998.0
// 平均价格: 5499.0
// 最低价格: 3999.0
// 最高价格: 6999.0
// ...

收集器组合

Collectors 类提供了许多方法可以组合起来创建更复杂的收集操作:

java
// 按类别分组,并进一步按价格区间分类
Map<String, Map<String, List<Order>>> categoryAndPriceRange = orders.stream()
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.groupingBy(order -> {
if (order.getAmount() < 500) return "低价";
else if (order.getAmount() < 3000) return "中价";
else return "高价";
})
));

System.out.println("\n按类别和价格区间分组:");
categoryAndPriceRange.forEach((category, priceMap) -> {
System.out.println(category + ":");
priceMap.forEach((priceRange, orderList) -> {
System.out.println(" " + priceRange + ": " +
orderList.stream().map(Order::getProduct).collect(Collectors.joining(", ")));
});
});
小技巧

从 Java 16 开始,你可以使用 Stream 的 toList() 方法作为 collect(Collectors.toList()) 的简化版本:

java
List<String> list = Stream.of("苹果", "香蕉", "橙子").toList();

总结

Java Stream 的收集操作是一个强大的功能,它允许我们以声明式的方式对数据进行复杂的转换和聚合:

  1. 基本收集:将 Stream 转换为集合、数组或字符串
  2. 数据转换:转换为 Map,进行键值对映射
  3. 分组和分区:按照条件对数据进行分类
  4. 统计聚合:计算求和、平均值、最大最小值等
  5. 自定义收集器:创建满足特定需求的收集操作

掌握 Stream 收集操作可以帮助我们编写更加简洁、高效的代码,特别是在处理大量数据和复杂业务逻辑时。

练习题

  1. 创建一个学生列表,包含姓名和分数,使用 Stream 收集操作找出分数最高的学生。
  2. 按照学生的成绩范围(优秀:90-100,良好:80-89,中等:70-79,及格:60-69,不及格:<60)对学生进行分组。
  3. 计算每个班级的平均分数,并找出平均分最高的班级。

进一步学习资源

通过不断实践和探索,你会发现 Stream 收集操作在日常开发中的强大之处!