Java 8引入了java.util.stream
包,极大地丰富了Java的集合操作能力。Stream
API提供了一种高效且易读的方式来处理集合数据。它的设计灵感来自于函数式编程和SQL查询操作,让我们能够以声明式的方式处理数据流。
Stream的核心概念和类型
Stream
依然不存储数据,它主要用于对集合数据进行检索和逻辑处理,包括筛选、排序、统计、计数等操作。最常见的流类型有两种:
- 串行流(Serial Stream):使用单线程顺序执行。
- 并行流(Parallel Stream):使用多线程并行执行,提高处理速度。
Stream的常用方法
创建Stream
java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream(); // 串行流
Stream<String> parallelNameStream = names.parallelStream(); // 并行流
Stream<String> singleValueStream = Stream.of("Alice"); // 创建流
Stream<String> arrayStream = Stream.of(new String[]{"Alice", "Bob", "Charlie"}); // 从数组创建流
过滤和匹配
java
List<String> names = Arrays.asList("Anna", "Bob", "Cathy", "Anna");
// 过滤
Stream<String> filteredStream = names.stream().filter(name -> "Anna".equals(name));
// 匹配
boolean allMatch = names.stream().allMatch(name -> name.length() == 4);
boolean anyMatch = names.stream().anyMatch(name -> name.startsWith("A"));
收集和统计
java
List<String> filteredList = names.stream()
.filter(name -> "Anna".equals(name))
.collect(Collectors.toList());
String mergedNames = names.stream()
.filter(name -> !name.isEmpty())
.collect(Collectors.joining(","));
// 统计
List<Integer> numbers = Arrays.asList(2, 4, 7, 3);
IntSummaryStatistics stats = numbers.stream()
.mapToInt(n -> n)
.summaryStatistics();
System.out.println("最大值: " + stats.getMax());
System.out.println("最小值: " + stats.getMin());
System.out.println("平均值: " + stats.getAverage());
System.out.println("总和: " + stats.getSum());
转换和操作
java
// 转换
Stream<String> mappedStream = names.stream().map(name -> name + "123");
// 排序
names.stream().sorted().forEach(System.out::println);
// 限制和跳过
Stream<String> limitedStream = names.stream().limit(1);
Stream<String> skippedStream = names.stream().skip(1);
// 去重
Stream<String> distinctStream = names.stream().distinct();
// 合并流
List<String> moreNames = Arrays.asList("Dave", "Eva");
Stream<String> combinedStream = Stream.concat(names.stream(), moreNames.stream());
中间操作与终端操作
Stream
操作分为中间操作和终端操作。中间操作返回一个新的流,可以链式调用;终端操作触发流的处理,并返回最终结果。
java
List<String> names = Arrays.asList("Anna", "Bob", "Cathy", "Anna");
// 中间操作
Stream<String> intermediateStream = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase);
// 终端操作
long count = intermediateStream.count();
实战示例
以下示例展示了如何使用Stream
API执行一系列常见操作:
java
@Test
public void testStreamAPI() {
List<String> names = Arrays.asList("Anna", "Bob", "Cathy", "Anna");
// 过滤并计数
long count = names.stream()
.filter(name -> "Anna".equals(name))
.count();
System.out.println("Count of 'Anna': " + count);
// 遍历并打印
names.stream()
.forEach(System.out::println);
// 限制结果
String[] limitedArray = names.stream()
.limit(1)
.toArray(String[]::new);
System.out.println("Limited array: " + Arrays.toString(limitedArray));
// 映射操作
names.stream()
.map(name -> name + "123")
.forEach(System.out::println);
// 排序并打印
names.stream()
.sorted()
.forEach(System.out::println);
// 收集到列表
List<String> collectedList = names.stream()
.filter("Anna"::equals)
.collect(Collectors.toList());
System.out.println("Collected list: " + collectedList);
// 合并流并计数
List<String> moreNames = Arrays.asList("Dave", "Eva");
long combinedCount = Stream.concat(names.stream(), moreNames.stream()).count();
System.out.println("Combined count: " + combinedCount);
// 使用统计信息
List<Integer> numbers = Arrays.asList(2, 4, 7, 3);
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Max: " + stats.getMax());
System.out.println("Min: " + stats.getMin());
System.out.println("Average: " + stats.getAverage());
System.out.println("Sum: " + stats.getSum());
}
并行流和串行流
在Java 8中,Stream
API提供了两种主要类型的流:串行流(Serial Stream)和并行流(Parallel Stream)。这两种流的设计旨在满足不同的性能需求和应用场景。
串行流(Serial Stream)
特点
- 单线程执行:串行流在单个线程中按顺序处理每个元素。
- 易于理解和调试:由于操作是按顺序执行的,所以更容易理解和调试。
- 没有线程安全问题:由于没有并发操作,不需要担心线程安全问题。
适用场景
- 小数据集:在处理较小的数据集时,串行流通常已经足够高效,不需要引入并行处理的复杂性。
- 顺序依赖的操作:当操作之间存在顺序依赖时(例如,文件读写操作、数据库事务等),串行流是更好的选择。
示例
java
List<String> data = Arrays.asList("a", "b", "c", "d");
long count = data.stream()
.filter(s -> s.startsWith("a"))
.count();
System.out.println("Count: " + count);
并行流(Parallel Stream)
特点
- 多线程执行:并行流利用多个线程同时处理流中的元素,通常基于Java的Fork/Join框架。
- 提高性能:在多核处理器上,并行流可以显著提高处理大数据集的性能。
- 复杂性增加:需要注意线程安全和共享资源的问题。
适用场景
- 大数据集:在处理非常大的数据集时,并行流可以显著提高性能。
- 计算密集型任务:对于CPU密集型的操作,并行流能够更好地利用多核处理器的优势。
- 无状态操作:并行流中的操作应尽量是无状态的,避免修改共享状态,以减少同步开销。
示例
java
List<String> data = Arrays.asList("a", "b", "c", "d");
long count = data.parallelStream()
.filter(s -> s.startsWith("a"))
.count();
System.out.println("Count: " + count);
并行流的实现原理
并行流的实现依赖于Java的Fork/Join框架。Fork/Join框架是一种用于并行执行任务的框架,它将大任务拆分为多个小任务(fork),并将结果合并(join)。并行流通过这种方式实现了数据的并行处理。
核心类和方法
ForkJoinPool
并行流默认使用Java的公共ForkJoinPool
,该池中的线程数量等于机器的可用处理器数量(Runtime.getRuntime().availableProcessors())。
java
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println("默认并行度: " + commonPool.getParallelism());
Spliterator
Spliterator
是Java 8引入的一个接口,用于分割数据源。并行流使用Spliterator
将数据源分割成多个部分,并行执行。
示例:并行流与串行流的性能对比
java
import java.util.*;
import java.util.stream.*;
import java.time.*;
public class StreamPerformanceTest {
public static void main(String[] args) {
List<Integer> largeList = IntStream.range(0, 1000000)
.boxed()
.collect(Collectors.toList());
// 使用串行流处理
Instant start = Instant.now();
long serialSum = largeList.stream()
.reduce(0, Integer::sum);
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
System.out.println("串行流处理时间: " + timeElapsed.toMillis() + " ms");
// 使用并行流处理
start = Instant.now();
long parallelSum = largeList.parallelStream()
.reduce(0, Integer::sum);
end = Instant.now();
timeElapsed = Duration.between(start, end);
System.out.println("并行流处理时间: " + timeElapsed.toMillis() + " ms");
// 确保结果一致
System.out.println("串行流结果: " + serialSum);
System.out.println("并行流结果: " + parallelSum);
}
}
在以上示例中,我们通过计算一个大列表的元素总和来比较串行流和并行流的处理时间。根据数据集大小和任务的复杂性,您会发现并行流通常能显著缩短处理时间。
并行流的线程数
并行流的线程数并不是固定的,而是由Java的公共ForkJoinPool
(ForkJoinPool.commonPool())决定的。默认情况下,这个公共池的线程数量等于机器的可用处理器数量,可以通过Runtime.getRuntime().availableProcessors()
来获取。
默认并行线程数
默认情况下,并行流会使用ForkJoinPool.commonPool()
,其并行度等于系统的可用处理器数量。例如:
java
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println("默认并行度: " + commonPool.getParallelism());
在多核处理器上,这个值通常等于处理器的核心数。
自定义并行线程数
虽然默认的线程数是由系统的可用处理器数量决定的,但我们可以通过自定义ForkJoinPool
来调整并行流的线程数。
使用自定义的ForkJoinPool
我们可以使用自定义的ForkJoinPool
来执行并行流操作,以控制并行度。以下是一个如何自定义并行线程数的示例:
java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class CustomParallelStream {
public static void main(String[] args) {
List<Integer> numbers = IntStream.range(0, 1000)
.boxed()
.collect(Collectors.toList());
// 创建自定义的ForkJoinPool
ForkJoinPool customThreadPool = new ForkJoinPool(4); // 自定义并行线程数为4
try {
// 使用自定义ForkJoinPool来执行并行流操作
long sum = customThreadPool.submit(() ->
numbers.parallelStream().reduce(0, Integer::sum)
).get();
System.out.println("Sum: " + sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
customThreadPool.shutdown();
}
}
}
在以上示例中,我们创建了一个自定义的ForkJoinPool
,并使用它来执行并行流操作。通过这种方式,我们可以灵活地控制并行流的线程数。
ForkJoinPool的核心参数
ForkJoinPool
的构造函数允许我们指定并行度(parallelism),即线程池中活跃线程的数量。其核心构造函数如下:
java
public ForkJoinPool(int parallelism);
例如,创建一个具有8个并行度的ForkJoinPool
:
java
ForkJoinPool customThreadPool = new ForkJoinPool(8);
注意事项
- 流的操作是惰性的:流的中间操作(如
filter
、map
)是惰性的,只有在终端操作(如forEach
、count
)执行时才会实际处理数据。 - 流只能使用一次:一旦流被操作过(如调用了
count
),它就不能再被操作,否则会抛出IllegalStateException
。可以通过重新创建流来避免这个问题。 - 并行流的线程安全:并行流中的操作应尽量无状态或线程安全,以避免并发问题。
- 性能开销:并行流的线程管理和任务拆分会带来额外的性能开销,对于小数据集可能得不偿失。
- 正确性:在某些情况下,并行流可能会改变操作的语义,例如,顺序相关的操作(
limit
、findFirst
)在并行流中可能会产生不同的结果。
小结
通过本文,我们详细介绍了Java 8 Stream API的核心概念和用法,并通过实战示例深入理解了如何在实际应用中使用Stream
进行数据处理。我们还探讨了串行流和并行流的区别、各自的适用场景以及如何自定义并行流的线程数。
总结
- 串行流和并行流:串行流适用于小数据集和顺序依赖的操作,并行流适用于大数据集和计算密集型任务。
- 并行流的实现:并行流利用Fork/Join框架实现多线程并行处理,默认使用公共
ForkJoinPool
,并行度等于系统的可用处理器数量。 - 自定义并行度:可以通过自定义
ForkJoinPool
来调整并行流的并行度,以更好地控制应用程序的并行行为。 - 注意事项:流的操作是惰性的,流只能使用一次,并行流的操作应尽量无状态或线程安全,避免性能开销和正确性问题。
通过合理使用Stream API,我们可以编写更加简洁、高效的代码,提高代码的可读性和维护性。同时,在适当的场景下使用并行流,可以显著提升程序的性能。