Stream 终结操作
2025/9/17大约 11 分钟
Java Stream API 终结操作详解
什么是终结操作?
终结操作(Terminal Operations)是 Stream 流水线的最后一步,它会触发流的执行并产生结果。与中间操作不同,终结操作会消费流,使其不能再被使用。每个 Stream 只能有一个终结操作,当这个操作执行后,流就被消费掉了。
终结操作的主要特点:
- 触发执行:终结操作会触发所有中间操作的执行
- 产生结果:终结操作会产生一个结果或副作用
- 消费流:执行终结操作后,流就被消费掉了,不能再使用
常用终结操作
1. forEach - 遍历元素
forEach
方法接收一个 Consumer 函数式接口,对流中的每个元素执行指定的操作。
import java.util.Arrays;
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
// 使用 forEach 遍历元素
fruits.stream()
.forEach(fruit -> System.out.println("Fruit: " + fruit));
// 使用方法引用
System.out.println("\n使用方法引用:");
fruits.stream()
.forEach(System.out::println);
}
}
2. count - 计算元素个数
count
方法返回流中元素的数量。
import java.util.Arrays;
import java.util.List;
public class CountExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 计算所有元素的数量
long total = numbers.stream().count();
System.out.println("总数量: " + total);
// 计算偶数的数量
long evenCount = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
System.out.println("偶数数量: " + evenCount);
}
}
3. collect - 收集结果
collect
方法是最强大的终结操作之一,它可以将流转换为不同的集合类型,或者执行聚合操作。
3.1 收集为 List
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CollectToListExample {
public static void main(String[] args) {
String[] array = {"Apple", "Banana", "Cherry", "Date"};
// 收集为 List
List<String> list = Arrays.stream(array)
.filter(s -> s.startsWith("A") || s.startsWith("B"))
.collect(Collectors.toList());
System.out.println("收集为 List: " + list);
}
}
3.2 收集为 Set
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectToSetExample {
public static void main(String[] args) {
String[] array = {"Apple", "Banana", "Apple", "Cherry", "Banana", "Date"};
// 收集为 Set(自动去重)
Set<String> set = Arrays.stream(array)
.collect(Collectors.toSet());
System.out.println("收集为 Set: " + set);
}
}
3.3 收集为 Map
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class CollectToMapExample {
public static void main(String[] args) {
String[] array = {"Apple", "Banana", "Cherry", "Date"};
// 收集为 Map(键为字符串,值为长度)
Map<String, Integer> map = Arrays.stream(array)
.collect(Collectors.toMap(
s -> s, // 键映射函数
String::length // 值映射函数
));
System.out.println("收集为 Map: " + map);
}
}
3.4 分组
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingByExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList(
"Apple", "Apricot", "Banana", "Blackberry", "Cherry", "Coconut"
);
// 按首字母分组
Map<Character, List<String>> groupByFirstLetter = fruits.stream()
.collect(Collectors.groupingBy(
s -> s.charAt(0)
));
System.out.println("按首字母分组: " + groupByFirstLetter);
}
}
3.5 聚合统计
import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
public class SummarizingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 收集统计信息
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println("统计信息: " + stats);
System.out.println("计数: " + stats.getCount());
System.out.println("总和: " + stats.getSum());
System.out.println("最小值: " + stats.getMin());
System.out.println("最大值: " + stats.getMax());
System.out.println("平均值: " + stats.getAverage());
}
}
3.6 字符串拼接
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class JoiningExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
// 简单拼接
String joined = fruits.stream()
.collect(Collectors.joining());
System.out.println("简单拼接: " + joined);
// 使用分隔符
String joinedWithDelimiter = fruits.stream()
.collect(Collectors.joining(", "));
System.out.println("使用分隔符: " + joinedWithDelimiter);
// 使用前缀和后缀
String joinedWithPrefixSuffix = fruits.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("使用前缀和后缀: " + joinedWithPrefixSuffix);
}
}
4. reduce - 归约操作
reduce
方法将流中的元素组合起来,得到一个值。
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 1. 使用初始值的 reduce
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("总和: " + sum);
// 使用方法引用
int sum2 = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("总和(使用方法引用): " + sum2);
// 2. 不使用初始值的 reduce
Optional<Integer> product = numbers.stream()
.reduce((a, b) -> a * b);
product.ifPresent(p -> System.out.println("乘积: " + p));
// 3. 更复杂的 reduce 操作
int sumOfSquares = numbers.stream()
.reduce(0, (sum1, num) -> sum1 + num * num, Integer::sum);
System.out.println("平方和: " + sumOfSquares);
}
}
5. min/max - 查找最小/最大值
min
和 max
方法根据指定的比较器返回流中的最小或最大元素。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MinMaxExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date", "Elderberry");
// 查找最短的字符串
Optional<String> shortest = fruits.stream()
.min(Comparator.comparing(String::length));
shortest.ifPresent(s -> System.out.println("最短的字符串: " + s));
// 查找最长的字符串
Optional<String> longest = fruits.stream()
.max(Comparator.comparing(String::length));
longest.ifPresent(s -> System.out.println("最长的字符串: " + s));
// 使用自然排序
Optional<String> first = fruits.stream()
.min(String::compareTo);
first.ifPresent(s -> System.out.println("字典序第一个: " + s));
Optional<String> last = fruits.stream()
.max(String::compareTo);
last.ifPresent(s -> System.out.println("字典序最后一个: " + s));
}
}
6. findFirst/findAny - 查找元素
findFirst
返回流中的第一个元素,findAny
返回流中的任意一个元素。
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class FindExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date", "Elderberry");
// 查找第一个元素
Optional<String> first = fruits.stream().findFirst();
first.ifPresent(s -> System.out.println("第一个元素: " + s));
// 查找任意元素(串行流中通常返回第一个元素)
Optional<String> any = fruits.stream().findAny();
any.ifPresent(s -> System.out.println("任意元素: " + s));
// 在并行流中,findAny 可能返回任意元素
Optional<String> anyParallel = fruits.parallelStream().findAny();
anyParallel.ifPresent(s -> System.out.println("并行流中的任意元素: " + s));
// 查找第一个以 'C' 开头的水果
Optional<String> firstC = fruits.stream()
.filter(s -> s.startsWith("C"))
.findFirst();
firstC.ifPresent(s -> System.out.println("第一个以 'C' 开头的水果: " + s));
}
}
7. anyMatch/allMatch/noneMatch - 匹配判断
这三个方法用于检查流中的元素是否满足特定条件:
anyMatch
:检查是否至少有一个元素满足条件allMatch
:检查是否所有元素都满足条件noneMatch
:检查是否没有元素满足条件
import java.util.Arrays;
import java.util.List;
public class MatchExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
// 检查是否至少有一个偶数
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("是否包含偶数: " + hasEven);
// 检查是否全部是偶数
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println("是否全部是偶数: " + allEven);
// 检查是否没有奇数
boolean noOdd = numbers.stream().noneMatch(n -> n % 2 != 0);
System.out.println("是否没有奇数: " + noOdd);
// 检查是否全部大于 0
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("是否全部大于 0: " + allPositive);
// 检查是否没有负数
boolean noNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("是否没有负数: " + noNegative);
}
}
8. toArray - 转换为数组
toArray
方法将流转换为数组。
import java.util.Arrays;
import java.util.List;
public class ToArrayExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
// 转换为 Object 数组
Object[] objectArray = fruits.stream().toArray();
System.out.println("Object 数组: " + Arrays.toString(objectArray));
// 转换为指定类型的数组
String[] stringArray = fruits.stream().toArray(String[]::new);
System.out.println("String 数组: " + Arrays.toString(stringArray));
// 转换为指定类型的数组(带过滤)
String[] filteredArray = fruits.stream()
.filter(s -> s.length() > 5)
.toArray(String[]::new);
System.out.println("过滤后的数组: " + Arrays.toString(filteredArray));
}
}
并行流
并行流(Parallel Stream)允许我们利用多核处理器的优势,对流中的元素进行并行处理,从而提高性能。
1. 创建并行流
有两种方式创建并行流:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class ParallelStreamCreationExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 方法 1:从集合创建并行流
Stream<Integer> parallelStream1 = numbers.parallelStream();
// 方法 2:将顺序流转换为并行流
Stream<Integer> parallelStream2 = numbers.stream().parallel();
// 检查流是否为并行
boolean isParallel1 = parallelStream1.isParallel();
boolean isParallel2 = parallelStream2.isParallel();
System.out.println("parallelStream1 是并行的: " + isParallel1);
System.out.println("parallelStream2 是并行的: " + isParallel2);
}
}
2. 并行流性能比较
并行流与顺序流性能比较
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ParallelPerformanceExample {
private static final int DATA_SIZE = 10_000_000;
public static void main(String[] args) {
// 准备测试数据
List<Integer> data = new ArrayList<>(DATA_SIZE);
for (int i = 0; i < DATA_SIZE; i++) {
data.add(i);
}
// 测试顺序流性能
long sequentialStart = System.nanoTime();
long sequentialSum = data.stream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> n * n)
.sum();
long sequentialEnd = System.nanoTime();
long sequentialTime = TimeUnit.NANOSECONDS.toMillis(sequentialEnd - sequentialStart);
// 测试并行流性能
long parallelStart = System.nanoTime();
long parallelSum = data.parallelStream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> n * n)
.sum();
long parallelEnd = System.nanoTime();
long parallelTime = TimeUnit.NANOSECONDS.toMillis(parallelEnd - parallelStart);
// 输出结果
System.out.println("数据大小: " + DATA_SIZE);
System.out.println("顺序流结果: " + sequentialSum + ", 耗时: " + sequentialTime + " ms");
System.out.println("并行流结果: " + parallelSum + ", 耗时: " + parallelTime + " ms");
System.out.println("性能提升: " + (double)sequentialTime / parallelTime + "x");
}
}
3. 并行流注意事项
并行流虽然可以提高性能,但也有一些注意事项:
3.1 顺序敏感的操作
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ParallelOrderExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
numbers.add(i);
}
// 顺序流保持顺序
List<Integer> sequentialResult = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println("顺序流结果: " + sequentialResult);
// 并行流可能改变顺序
List<Integer> parallelResult = numbers.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println("并行流结果: " + parallelResult);
// 使用 forEachOrdered 保持顺序
System.out.print("使用 forEachOrdered: ");
numbers.parallelStream()
.map(n -> n * 2)
.forEachOrdered(n -> System.out.print(n + " "));
}
}
3.2 非线程安全的操作
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
public class ParallelThreadSafetyExample {
public static void main(String[] args) {
// 非线程安全示例
List<Integer> nonThreadSafeList = new ArrayList<>();
IntStream.range(0, 1000)
.parallel()
.forEach(nonThreadSafeList::add); // 可能导致线程安全问题
System.out.println("非线程安全列表大小: " + nonThreadSafeList.size()); // 可能小于 1000
// 线程安全示例 1:使用同步列表
List<Integer> threadSafeList1 = Collections.synchronizedList(new ArrayList<>());
IntStream.range(0, 1000)
.parallel()
.forEach(threadSafeList1::add);
System.out.println("线程安全列表 1 大小: " + threadSafeList1.size()); // 应该是 1000
// 线程安全示例 2:使用 collect 操作
List<Integer> threadSafeList2 = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("线程安全列表 2 大小: " + threadSafeList2.size()); // 应该是 1000
}
}
3.3 适合并行的操作
import java.util.Arrays;
import java.util.List;
public class ParallelSuitableOperationsExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 适合并行的操作:独立的元素计算
long start = System.nanoTime();
double resultSeq = numbers.stream()
.map(ParallelSuitableOperationsExample::expensiveOperation)
.reduce(0.0, Double::sum);
long end = System.nanoTime();
System.out.println("顺序计算耗时: " + (end - start) / 1_000_000 + "ms");
start = System.nanoTime();
double resultPar = numbers.parallelStream()
.map(ParallelSuitableOperationsExample::expensiveOperation)
.reduce(0.0, Double::sum);
end = System.nanoTime();
System.out.println("并行计算耗时: " + (end - start) / 1_000_000 + "ms");
}
// 模拟计算密集型操作
private static double expensiveOperation(int n) {
double result = 0;
for (int i = 0; i < 1000000; i++) {
result += Math.sin(n) * Math.cos(n);
}
return result;
}
}
实际应用示例
1. 数据分析与统计
产品数据分析示例代码
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Data;
public class ProductAnalysis {
@Data
@AllArgsConstructor
static class Product {
private String name;
private String category;
private double price;
private int stock;
}
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("iPhone 13", "Electronics", 999.99, 50),
new Product("Samsung Galaxy S21", "Electronics", 899.99, 30),
new Product("MacBook Pro", "Electronics", 1999.99, 20),
new Product("Nike Air Max", "Footwear", 129.99, 100)
);
// 1. 按类别分组并计算每个类别的产品数量
Map<String, Long> productCountByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.counting()
));
System.out.println("各类别产品数量:");
productCountByCategory.forEach((category, count) ->
System.out.println(category + ": " + count)
);
// 2. 计算每个类别的平均价格
Map<String, Double> avgPriceByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.averagingDouble(Product::getPrice)
));
System.out.println("\n各类别平均价格:");
avgPriceByCategory.forEach((category, avgPrice) ->
System.out.printf("%s: $%.2f\n", category, avgPrice)
);
// 3. 计算库存价值总和
double totalInventoryValue = products.stream()
.mapToDouble(p -> p.getPrice() * p.getStock())
.sum();
System.out.printf("\n总库存价值: $%.2f\n", totalInventoryValue);
}
}
2. 文件处理
文件处理示例代码
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class FileProcessingExample {
public static void main(String[] args) {
try {
// 1. 统计单词频率
Map<String, Long> wordFrequency = Files.lines(Paths.get("example.txt"))
.flatMap(line -> Arrays.stream(line.toLowerCase().split("\\s+")))
.map(word -> word.replaceAll("[^a-zA-Z]", ""))
.filter(word -> !word.isEmpty())
.collect(Collectors.groupingBy(
word -> word,
Collectors.counting()
));
System.out.println("单词频率(前 10 个):");
wordFrequency.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
// 2. 统计每行的字符数和过滤特定内容
System.out.println("\n包含 'Java' 的行:");
Files.lines(Paths.get("example.txt"))
.filter(line -> line.contains("Java"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
最佳实践
性能考虑
- 选择合适的终结操作:不同的终结操作有不同的性能特点
- 避免装箱/拆箱:对于基本类型,使用专门的流(IntStream, LongStream, DoubleStream)
- 合理使用并行流:只在适当的场景下使用并行流
- 注意有状态操作:有状态操作(如 sorted)在并行流中可能影响性能
实施建议
- 优先使用 collect 而非 forEach:collect 更灵活,可以生成各种集合类型
- 使用方法引用:当可能时,使用方法引用代替 Lambda 表达式
- 避免副作用:保持操作的纯函数特性,不要修改外部状态
- 合理组合操作:尽早进行过滤操作,减少后续处理的元素数量
常见问题
1. 如何处理 Optional 结果?
终结操作如 min、max、findFirst 返回 Optional 对象。处理方式:
- ifPresent:值存在时执行操作
- orElse:提供默认值
- orElseGet:延迟计算默认值
- orElseThrow:值不存在时抛出异常
- map:转换值
// 使用 ifPresent
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
max.ifPresent(value -> System.out.println("最大值: " + value));
// 使用 orElse 提供默认值
int min = numbers.stream().min(Integer::compareTo).orElse(0);
// 使用 map 转换值
String maxAsString = numbers.stream()
.max(Integer::compareTo)
.map(String::valueOf)
.orElse("N/A");
2. 如何在流操作中处理异常?
在流操作中处理异常有两种主要方法:
- 在映射函数中直接处理异常:
List<String> fileContents = filePaths.stream()
.map(path -> {
try {
return new String(Files.readAllBytes(Paths.get(path)));
} catch (IOException e) {
return "Error reading file: " + path;
}
})
.collect(Collectors.toList());
- 使用包装函数处理异常:
// 异常包装函数
private static <T, R> Function<T, R> wrap(CheckedFunction<T, R> function) {
return t -> {
try {
return function.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
总结
本教程详细介绍了 Java Stream API 的终结操作和并行流处理,包括:
- ✅ 终结操作的概念和特点:触发执行、产生结果、消费流
- ✅ 常用终结操作:forEach、count、collect、reduce、min/max、findFirst/findAny、anyMatch/allMatch/noneMatch、toArray
- ✅ 并行流:创建方式、性能比较、注意事项
- ✅ 实际应用示例:数据分析与统计、文件处理
- ✅ 最佳实践和常见问题:性能考虑、实施建议、Optional 处理、异常处理
下一步学习
希望这个教程对您有所帮助!