Java Stream 终端操作
Stream API 是 Java 8 引入的一种处理数据集合的强大功能。在使用 Stream 进行数据处理时,我们通常会经历三个阶段:数据源获取、中间操作和终端操作。本文将重点讨论终端操作,这是整个 Stream 流水线的最后一步,也是触发整个流计算执行的关键步骤。
什么是终端操作?
终端操作是流处理的最后一个环节,它会从流中产生一个结果,并在操作完成后关闭该流。一旦执行了终端操作,流就被消费掉了,不能再被使用。
终端操作的特点是产生一个结果(可能是值、集合或者无返回值),而非另一个 Stream。
常用的终端操作分类
我们可以将终端操作分为以下几类:
- 收集操作:将流元素转换为集合或其他数据结构
- 匹配和查找操作:用于检查元素是否符合特定条件
- 归约操作:将流中的元素组合成单个结果
- 遍历操作:对每个元素执行操作而不返回结果
- 计数操作:计算流中元素的数量
接下来,我们将详细介绍这些操作。
收集操作
收集操作主要通过 collect()
方法实现,该方法接收一个 Collector
接口的实现,用于将流元素收集到不同的数据结构中。
collect() 方法
collect()
是最强大的终端操作之一,通常与 Collectors
工具类一起使用。以下是一些常见的收集操作:
收集到 List
List<String> names = Stream.of("John", "Mary", "Bob", "Alice")
.collect(Collectors.toList());
System.out.println(names); // 输出: [John, Mary, Bob, Alice]
收集到 Set
Set<String> uniqueNames = Stream.of("John", "Mary", "John", "Alice")
.collect(Collectors.toSet());
System.out.println(uniqueNames); // 输出: [John, Mary, Alice](顺序可能不同)
收集到 Map
Map<String, Integer> nameLengths = Stream.of("John", "Mary", "Bob")
.collect(Collectors.toMap(
name -> name, // 键映射函数
name -> name.length() // 值映射函数
));
System.out.println(nameLengths); // 输出: {Bob=3, John=4, Mary=4}
分组
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Mary", 30),
new Person("Bob", 25)
);
Map<Integer, List<Person>> peopleByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println(peopleByAge);
// 输出: {25=[Person{name=John, age=25}, Person{name=Bob, age=25}], 30=[Person{name=Mary, age=30}]}
字符串拼接
String joinedNames = Stream.of("John", "Mary", "Bob")
.collect(Collectors.joining(", "));
System.out.println(joinedNames); // 输出: John, Mary, Bob
匹配和查找操作
匹配操作用于检查是否有元素满足特定条件,而查找操作则用于获取满足条件的元素。
匹配操作
Java Stream API 提供了三种匹配操作:
anyMatch()
检查是否至少有一个元素满足条件:
boolean hasNameWithA = Stream.of("John", "Mary", "Bob", "Alice")
.anyMatch(name -> name.contains("a"));
System.out.println(hasNameWithA); // 输出: true (Mary 和 Alice 包含字母 'a')
allMatch()
检查是否所有元素都满足条件:
boolean allNamesLongerThan2 = Stream.of("John", "Mary", "Bob", "Alice")
.allMatch(name -> name.length() > 2);
System.out.println(allNamesLongerThan2); // 输出: true
noneMatch()
确保没有元素满足条件:
boolean noNameStartsWithX = Stream.of("John", "Mary", "Bob", "Alice")
.noneMatch(name -> name.startsWith("X"));
System.out.println(noNameStartsWithX); // 输出: true
查找操作
findFirst()
返回流中的第一个元素,结果是 Optional
类型:
Optional<String> firstNameWithA = Stream.of("John", "Mary", "Bob", "Alice")
.filter(name -> name.contains("a"))
.findFirst();
System.out.println(firstNameWithA.orElse("No name found")); // 输出: Mary
findAny()
返回流中的任意一个元素,不保证返回第一个。在并行流中特别有用:
Optional<String> anyNameWithA = Stream.of("John", "Mary", "Bob", "Alice")
.filter(name -> name.contains("a"))
.findAny();
System.out.println(anyNameWithA.orElse("No name found")); // 可能输出 Mary 或 Alice
归约操作
归约操作用于将一组值组合成单个结果,例如计算总和、平均值或连接字符串。
reduce() 方法
reduce()
方法接收两个参数:一个初始值和一个 BinaryOperator
函数,用于将两个元素合并。
int sum = Stream.of(1, 2, 3, 4, 5)
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 输出: 15
// 使用方法引用
int product = Stream.of(1, 2, 3, 4, 5)
.reduce(1, Math::multiplyExact);
System.out.println(product); // 输出: 120
也可以使用不接受初始值的 reduce()
重载版本,此时返回类型为 Optional
:
Optional<Integer> max = Stream.of(1, 5, 3, 7, 2)
.reduce(Math::max);
System.out.println(max.orElse(0)); // 输出: 7
其他常见终端操作
count()
返回流中的元素数量:
long count = Stream.of("John", "Mary", "Bob", "Alice")
.count();
System.out.println(count); // 输出: 4
forEach()
对每个元素执行操作,没有返回值:
Stream.of("John", "Mary", "Bob", "Alice")
.forEach(name -> System.out.println("Hello, " + name));
// 输出:
// Hello, John
// Hello, Mary
// Hello, Bob
// Hello, Alice
min() 和 max()
找出流中基于指定比较器的最小或最大元素:
Optional<String> shortestName = Stream.of("John", "Mary", "Bob", "Alice")
.min(Comparator.comparing(String::length));
System.out.println(shortestName.orElse("None")); // 输出: Bob
Optional<String> longestName = Stream.of("John", "Mary", "Bob", "Alice")
.max(Comparator.comparing(String::length));
System.out.println(longestName.orElse("None")); // 输出: Alice
toArray()
将流转换为数组:
String[] namesArray = Stream.of("John", "Mary", "Bob", "Alice")
.toArray(String[]::new);
System.out.println(Arrays.toString(namesArray)); // 输出: [John, Mary, Bob, Alice]
实际案例:电子商务订单处理
让我们通过一个电子商务应用程序的订单处理实例,展示如何使用 Stream 的终端操作。
class Order {
private int id;
private String customer;
private double amount;
private String status;
public Order(int id, String customer, double amount, String status) {
this.id = id;
this.customer = customer;
this.amount = amount;
this.status = status;
}
public int getId() { return id; }
public String getCustomer() { return customer; }
public double getAmount() { return amount; }
public String getStatus() { return status; }
@Override
public String toString() {
return "Order{" +
"id=" + id +
", customer='" + customer + '\'' +
", amount=" + amount +
", status='" + status + '\'' +
'}';
}
}
现在,让我们使用 Stream 终端操作来处理订单列表:
List<Order> orders = Arrays.asList(
new Order(1, "John", 150.50, "COMPLETED"),
new Order(2, "Mary", 250.75, "PENDING"),
new Order(3, "John", 120.30, "COMPLETED"),
new Order(4, "Alice", 300.00, "CANCELLED"),
new Order(5, "Bob", 180.40, "COMPLETED")
);
// 1. 查找所有已完成订单的总金额
double totalCompletedAmount = orders.stream()
.filter(order -> order.getStatus().equals("COMPLETED"))
.mapToDouble(Order::getAmount)
.sum();
System.out.println("已完成订单总金额: " + totalCompletedAmount);
// 输出: 已完成订单总金额: 451.2
// 2. 按客户分组,计算每个客户的订单总数
Map<String, Long> orderCountByCustomer = orders.stream()
.collect(Collectors.groupingBy(
Order::getCustomer,
Collectors.counting()
));
System.out.println("每个客户的订单数: " + orderCountByCustomer);
// 输出: 每个客户的订单数: {Bob=1, John=2, Alice=1, Mary=1}
// 3. 查找最大金额的订单
Optional<Order> maxAmountOrder = orders.stream()
.max(Comparator.comparing(Order::getAmount));
System.out.println("最大金额订单: " + maxAmountOrder.orElse(null));
// 输出: 最大金额订单: Order{id=4, customer='Alice', amount=300.0, status='CANCELLED'}
// 4. 按状态分组,计算每种状态的订单总金额
Map<String, Double> totalAmountByStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::getStatus,
Collectors.summingDouble(Order::getAmount)
));
System.out.println("每种状态的订单总金额: " + totalAmountByStatus);
// 输出: 每种状态的订单总金额: {PENDING=250.75, COMPLETED=451.2, CANCELLED=300.0}
// 5. 检查是否所有订单金额都大于100
boolean allOrdersAbove100 = orders.stream()
.allMatch(order -> order.getAmount() > 100);
System.out.println("是否所有订单金额都大于100: " + allOrdersAbove100);
// 输出: 是否所有订单金额都大于100: true
终端操作和短路评估
一些终端操作(如 anyMatch()
、findFirst()
)可能不需要处理整个流就能产生结果。这称为短路评估,可以提高性能,特别是在处理大数据集或无限流时。
// 使用短路操作处理无限流
boolean found = Stream.iterate(1, n -> n + 1) // 创建无限流
.filter(n -> n % 17 == 0)
.anyMatch(n -> n > 100);
System.out.println(found); // 输出: true (不会无限执行)
总结
Stream API 的终端操作是触发整个流水线计算的关键环节。记住以下几点:
- 终端操作会消耗流,并产生最终结果(可能是值、集合或无返回值)
- 流一旦被消耗,就不能再被使用
- 常见的终端操作包括:收集(
collect()
)、归约(reduce()
)、匹配(anyMatch()
、allMatch()
、noneMatch()
)、查找(findFirst()
、findAny()
)以及其他如count()
、forEach()
、min()
、max()
和toArray()
- 选择合适的终端操作能够简化代码并提高性能
通过组合中间操作和终端操作,Java Stream API 提供了一种函数式的、声明式的方法来处理数据集合,使代码更加简洁、可读和高效。
练习
为了巩固所学知识,尝试完成以下练习:
- 给定一个整数列表,使用 Stream 操作找出所有偶数的平方和
- 给定一个字符串列表,使用 Stream 操作按字符串长度进行分组
- 给定一个
Person
对象列表(包含姓名和年龄),找出年龄最大的人 - 使用 Stream 操作检查一个整数列表是否包含任何重复元素
- 给定一个产品列表(包含名称、价格和类别),计算每个类别的平均价格
额外资源
如果你想深入学习 Stream API 的终端操作,可以参考以下资源:
实践是最好的学习方法!创建各种不同的实例来使用这些终端操作,这将帮助你更好地理解它们的功能和适用场景。