前言
Java 8 引入的 Stream API 是一项革命性的特性,它提供了一种高效且易于理解的数据处理方式。Stream API 可以让我们以声明式的方式处理集合数据,极大地简化了代码编写。其中,Stream 分为普通流和并行流,本文将深入探讨它们的原理、用法和适用场景,并通过具体的代码示例进行演示。
–
一、Stream 流概述
1.1 什么是 Stream 流
Stream 是 Java 8 中引入的一种新抽象概念,它代表着元素序列,并支持各种聚合操作。Stream 不是集合元素,它不是数据结构,不保存数据,而是对数据进行计算。
Stream 流的特点:
- 不存储数据:Stream 是对数据源的视图,不存储元素
- 函数式编程:Stream 操作不会修改源数据
- 延迟执行:中间操作返回新的 Stream,直到终止操作才执行计算
- 可消费性:Stream 只能被消费一次,消费后不能再次使用
1.2 Stream 流的基本操作流程
- 创建 Stream:从集合、数组等数据源创建 Stream
- 中间操作:对 Stream 进行处理,返回新的 Stream
- 终止操作:执行计算,产生结果
二、普通流(Sequential Stream)
2.1 创建普通流
普通流可以通过集合、数组等多种方式创建:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
// 从集合创建
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> streamFromList = list.stream();
// 从数组创建
String[] array = {"apple", "banana", "cherry"};
Stream<String> streamFromArray = Arrays.stream(array);
// 使用Stream.of
Stream<String> streamOf = Stream.of("apple", "banana", "cherry");
// 创建空Stream
Stream<String> emptyStream = Stream.empty();
}
}
2.2 中间操作
中间操作会返回一个新的 Stream,常见的中间操作有:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class IntermediateOperationsExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
// filter:过滤元素
Stream<String> filteredStream = list.stream()
.filter(s -> s.length() > 5);
// map:转换元素
Stream<Integer> lengthStream = list.stream()
.map(String::length);
// distinct:去重
Stream<String> distinctStream = list.stream()
.distinct();
// sorted:排序
Stream<String> sortedStream = list.stream()
.sorted();
// limit:限制元素数量
Stream<String> limitedStream = list.stream()
.limit(3);
// skip:跳过元素
Stream<String> skippedStream = list.stream()
.skip(2);
}
}
2.3 终止操作
终止操作会触发 Stream 的执行并产生结果,常见的终止操作有:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class TerminalOperationsExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
// forEach:遍历元素
list.stream()
.forEach(System.out::println);
// collect:收集元素到集合
List<String> filteredList = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
// toArray:转换为数组
String[] array = list.stream()
.toArray(String[]::new);
// count:统计元素数量
long count = list.stream()
.count();
// reduce:归约操作
Optional<String> reduced = list.stream()
.reduce((s1, s2) -> s1 + ", " + s2);
// anyMatch:是否存在匹配元素
boolean anyMatch = list.stream()
.anyMatch(s -> s.startsWith("b"));
// allMatch:是否所有元素都匹配
boolean allMatch = list.stream()
.allMatch(s -> s.length() > 3);
// noneMatch:是否所有元素都不匹配
boolean noneMatch = list.stream()
.noneMatch(s -> s.endsWith("z"));
// findFirst:查找第一个元素
Optional<String> first = list.stream()
.findFirst();
// findAny:查找任意元素
Optional<String> any = list.stream()
.findAny();
}
}
三、并行流(Parallel Stream)
3.1 什么是并行流
并行流是 Stream API 提供的一种高效处理大数据的方式,它利用多核处理器的优势,将数据分成多个片段,并行处理每个片段,最后合并结果。
并行流的特点:
- 自动并行化:底层自动利用多线程进行并行处理
- 基于 Fork/Join 框架:使用 Java 7 引入的 Fork/Join 框架实现并行计算
- 可能提高性能:在处理大数据时能显著提高性能
- 需要注意线程安全:并行流操作共享可变状态时需要特别注意
3.2 创建并行流
可以通过以下方式创建并行流:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class ParallelStreamCreationExample {
public static void main(String[] args) {
// 从集合创建并行流
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Stream<String> parallelStreamFromList = list.parallelStream();
// 将普通流转换为并行流
Stream<String> sequentialStream = list.stream();
Stream<String> parallelStream = sequentialStream.parallel();
// 从数组创建并行流
String[] array = {"apple", "banana", "cherry"};
Stream<String> parallelStreamFromArray = Arrays.stream(array).parallel();
}
}
3.3 并行流的性能测试
下面通过一个简单的例子测试并行流的性能:
import java.time.Duration;
import java.time.Instant;
import java.util.stream.LongStream;
public class ParallelStreamPerformanceExample {
public static void main(String[] args) {
long n = 1000000000;
// 普通流计算
Instant start1 = Instant.now();
long sum1 = LongStream.rangeClosed(1, n)
.sum();
Instant end1 = Instant.now();
System.out.println("普通流计算结果: " + sum1);
System.out.println("普通流耗时: " + Duration.between(start1, end1).toMillis() + " ms");
// 并行流计算
Instant start2 = Instant.now();
long sum2 = LongStream.rangeClosed(1, n)
.parallel()
.sum();
Instant end2 = Instant.now();
System.out.println("并行流计算结果: " + sum2);
System.out.println("并行流耗时: " + Duration.between(start2, end2).toMillis() + " ms");
}
}
3.4 并行流的适用场景
并行流适用于以下场景:
- 数据量大:数据量越大,并行流的优势越明显
- 计算密集型任务:不涉及 IO 操作的纯计算任务
- 无状态操作:操作不依赖于外部状态
- 可并行操作:操作可以分解为多个独立的子任务
四、普通流与并行流的对比
4.1 性能对比
场景 | 普通流 | 并行流 |
---|---|---|
小数据量 | 性能较好 | 可能有额外开销 |
大数据量 | 性能一般 | 性能显著提升 |
计算密集型任务 | 性能稳定 | 性能优势明显 |
IO 密集型任务 | 性能受 IO 限制 | 可能因线程切换导致性能下降 |
4.2 使用注意事项
- 避免共享可变状态:并行流在多线程环境下操作,共享可变状态会导致线程安全问题
- 合理使用并行流:不是所有场景都适合使用并行流,需要根据数据量和任务类型选择
- 注意操作顺序:并行流不保证元素的处理顺序,某些依赖顺序的操作可能产生意外结果
- 避免嵌套并行:嵌套并行流会导致线程爆炸,严重影响性能
五、常见问题与解决方案
5.1 线程安全问题
并行流在处理共享可变状态时会出现线程安全问题,例如:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class ThreadSafetyExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 错误示例:并行流操作非线程安全的集合
IntStream.range(0, 1000)
.parallel()
.forEach(i -> list.add(i));
System.out.println("预期大小: 1000, 实际大小: " + list.size());
}
}
解决方案:
- 使用线程安全的集合
- 使用
collect
或reduce
等安全的聚合操作
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ThreadSafetySolutionExample {
public static void main(String[] args) {
// 正确示例:使用collect操作
List<Integer> list = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("预期大小: 1000, 实际大小: " + list.size());
}
}
5.2 性能调优
- 避免不必要的并行:小数据量使用并行流可能反而更慢
- 使用合适的并行度:可以通过
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "N")
设置并行度 - 避免阻塞操作:并行流中的 IO 操作会导致线程长时间阻塞,降低性能
5.3 调试并行流
并行流的执行顺序不确定,调试时可能会遇到困难。可以通过以下方法辅助调试:
- 使用
peek
方法观察元素处理过程 - 打印线程信息,观察并行执行情况
import java.util.Arrays;
import java.util.List;
public class ParallelStreamDebuggingExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
list.parallelStream()
.peek(s -> System.out.println("Processing: " + s + " on thread: " + Thread.currentThread().getName()))
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
总结
Java Stream 流是处理集合数据的强大工具,普通流和并行流各有其适用场景:
- 普通流:适用于小数据量、简单操作、需要保持顺序的场景
- 并行流:适用于大数据量、计算密集型、无状态操作的场景
在使用并行流时,需要特别注意线程安全问题,避免共享可变状态。合理使用 Stream 流可以使代码更加简洁、高效,提高开发效率和程序性能。
通过本文的介绍和示例,希望读者能够深入理解普通流和并行流的原理和用法,在实际开发中灵活运用。