流 Stream

112 阅读13分钟

在 JDK 诸多版本中,Java 8绝对堪称一次里程碑式的升级,今天要讲的 Stream 流也是 Java 8 引入的。

⨳ Java 8 引入了基础的 Stream API,具有链式操作、并行处理、终端操作、惰性求值等功能;

⨳ JDK 9 在 Stream API 的基础上做了一些小的改进,新增的takeWhile()dropWhile() 这两个新方法允许根据条件进行短路操作;

⨳ JDK 10 对 Stream API 没有显著的变化,但它增强了类型推断,特别是通过 var 关键字让开发者更方便地声明变量;

⨳ JDK 11 增加了一些新的收集器,简化了数据聚合和转换操作;

⨳ ...

Stream API 提供了一个声明性编程模型来处理数据流。通过使用 Stream,开发者可以更简洁、高效地执行集合操作,如过滤、映射、规约等。

基本使用

// 过滤出所有的偶数,并将其平方,最后输出
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
        .filter(n -> n % 2 == 0)  // 过滤偶数
        .map(n -> n * n)          // 平方
        .forEach(System.out::println); // 输出

上述代码就是简单的对Stream的基本使用:

numbers.stream():将列表转换为流。

filter(n -> n % 2 == 0):筛选出偶数,并返回只有偶数的流。

map(n -> n * n):将全是偶数的流,进行平方操作,并返回一个新流。

forEach(System.out::println):遍历流中的每个元素,并执行 System.out.println 操作,进而打印每个结果。

如果不使用 Stream 流,上述代码就要使用常规循环遍历了:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        
        for (Integer number : numbers) {
            if (number % 2 == 0) {         // 过滤偶数
                int squared = number * number;  // 计算平方
                System.out.println(squared);    // 输出
            }
        }

可以看到,Stream 的链式调用让代码更简洁、更具有可读性。

在介绍 Stream 常用方法之前,先介绍两个概念:中间操作终端操作

⨳ 中间操作(Intermediate Operations)返回一个新的 Stream,通常用于将原始数据流转换或过滤为一个新流。这些操作是惰性求值的,意味着它们在没有终端操作时不会执行。如上述代码中的 filtermap

⨳ 终端操作 (Terminal Operations)触发流的计算,并返回一个结果。流在执行终端操作后就不能再使用。如上述代码中的 forEach

需要注意的是,Stream 提供的方法无论是中间操作还是终端操作都需要 Lambda 作为参数,所以要熟悉Steam,需要先了解Lambda表达式

创建 Stream

在 Java 中,创建 Stream 的方式有很多种,通常可以从集合中(List、Set、Queue 等)创建流;根据自定义值创建流;可以从文件中创建流,还可以使用生成器创建流,...

从集合中创建流

⨳ 从集合(Collection)创建 Stream

Java 中的 Collection 接口(如 ListSet 等)都有一个默认方法 stream()parallelStream(),可以创建顺序流或并行流。

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream(); // 顺序流
Stream<String> parallelStream = list.parallelStream(); // 并行流

⨳ 从数组创建 Stream

Arrays 类的 stream() 方法允许你从数组中创建流。

String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);

自定义值创建

⨳ 从空值或可选值创建 Stream

Stream<String> emptyStream = Stream.empty();

Stream<String> stream = Stream.ofNullable("a"); // 若为 null 则为空流

⨳ 使用 Stream.of() 创建 Stream

Stream.of() 方法允许从多个元素创建一个流。

Stream<String> stream = Stream.of("a", "b", "c");

⨳ 使用 Stream.builder() 创建 Stream

Stream.builder() 提供了一种更灵活的方式来构建流。

Stream<String> stream = Stream.<String>builder()
                              .add("a")
                              .add("b")
                              .add("c")
                              .build();

⨳ 从文件创建 Stream

使用 Files.lines() 可以从文件的每一行创建一个流。

Path path = Paths.get("file.txt");
Stream<String> lines = Files.lines(path);

使用生成器创建流

⨳ 使用 Stream.generate() 创建无限流

Stream.generate() 可以根据一个生成器函数创建无限流。

Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5);

⨳ 使用 Stream.iterate() 创建无限流或有界流

Stream.iterate() 可以通过初始值和函数生成一系列元素。

// 无限流
Stream<Integer> stream = Stream.iterate(0, n -> n + 2);
// 有界流 (Java 9+)
Stream<Integer> boundedStream = Stream.iterate(0, n -> n < 20, n -> n + 2);

多流合一

⨳ 从多个 Streams 合并

可以使用 Stream.concat() 将两个或多个流合并为一个流。

Stream<String> stream1 = Stream.of("a", "b");
Stream<String> stream2 = Stream.of("c", "d");
Stream<String> combinedStream = Stream.concat(stream1, stream2);

⨳ ...

这些创建方法灵活适应不同的数据源和需求。无论是从集合、数组、文件还是通过生成器函数,都能有效地创建流,并利用 Stream API 进行数据处理。

中间操作

中间操作(Intermediate Operations)不会触发流的处理,而是返回一个新的 Stream 对象,使得多个操作可以链式调用。

中间操作是惰性的,只有当终端操作(如 forEach()collect())被调用时,流的处理才真正执行。

过滤操作

⨳ filter()

Stream filter(Predicate<? super T> predicate);

filter() 用于对流中的元素进行筛选,保留满足条件的元素,生成一个新的流。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> filteredStream = stream.filter(n -> n % 2 == 0); // 筛选偶数

⨳ distinct()

Stream distinct();

distinct() 用于去除流中的重复元素,返回一个包含唯一元素的流。

Stream<Integer> stream = Stream.of(1, 2, 2, 3, 4, 4);
Stream<Integer> distinctStream = stream.distinct(); // 输出 [1, 2, 3, 4]

⨳ takeWhile()

Stream takeWhile(Predicate<? super T> predicate)

takeWhile() 根据一个条件(谓词)从流中提取元素,直到遇到不满足条件的元素为止。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> result = stream.takeWhile(n -> n < 4); // 输出 [1, 2, 3]

⨳ takeWhile()

Stream takeWhile(Predicate<? super T> predicate)

dropWhile()takeWhile() 相反,它会跳过流中前面所有满足条件的元素,直到遇到不满足条件的元素为止,返回剩余部分的流。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> result = stream.dropWhile(n -> n < 4); // 输出 [4, 5]

映射操作

⨳ map()

Stream map(Function<? super T, ? extends R> mapper);

map() 用于将流中的元素按照指定的函数进行映射,将元素转换为另一种类型。

Stream<String> stream = Stream.of("a", "b", "c");
Stream<String> mappedStream = stream.map(String::toUpperCase); // 转换为大写

⨳ flatMap()

Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

flatMap() 类似于 map(),但它将流中的每个元素映射为一个流,并将这些流合并成一个单一的流。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4));
Stream<Integer> flatMappedStream = stream.flatMap(List::stream); // 合并成一个流 [1, 2, 3, 4]

可能不容易理解,这里多说一句,flatMap()的作用是将多个小的 Stream 合并为一个单一的连续 Stream。

假设你有一个 Stream<List<Integer>>,使用 map() 只能将每个 List 映射为一个 Stream,这样的结果是生成了一个 Stream<Stream<Integer>>。这种形式实际上是 "流的流",在处理上会比较麻烦。

flatMap() 可以解决这个问题,它的作用是:

  1. 将每个 List 映射为一个 Stream<Integer>
  2. 然后将所有的小 Stream 扁平化为一个大的、连续的 Stream<Integer>
List<List<Integer>> listOfLists = Arrays.asList(
    Arrays.asList(1, 2, 3),
    Arrays.asList(4, 5),
    Arrays.asList(6, 7, 8, 9)
);

List<Integer> flatList = listOfLists.stream()
    .flatMap(list -> list.stream())  // 将 Stream<List<Integer>> 扁平化为 Stream<Integer>
    .collect(Collectors.toList());

System.out.println(flatList);  // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]

flatMap() 非常适合用于将嵌套结构(如列表嵌套列表、列表嵌套数组等)展开为一个单一的 Stream。

排序操作

⨳ sorted()

Stream sorted();

Stream sorted(Comparator<? super T> comparator);

sorted() 用于对流中的元素进行排序,默认按自然顺序排序,也可以使用自定义的比较器进行排序。

Stream<String> stream = Stream.of("banana", "apple", "orange");
Stream<String> sortedStream = stream.sorted(); // 自然排序
Stream<String> customSortedStream = stream.sorted(Comparator.reverseOrder()); // 自定义排序

限制与跳过操作

⨳ limit()

Stream limit(long maxSize);

limit() 用于截取流中的前 N 个元素,返回一个新流。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> limitedStream = stream.limit(3); // 输出 [1, 2, 3]

⨳ skip()

Stream skip(long n);

skip() 用于跳过流中的前 N 个元素,返回剩余的元素作为新流。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> skippedStream = stream.skip(2); // 输出 [3, 4, 5]

⨳ ...

常见中间操作包括过滤、映射(转换)、去重、截取等。这些操作之间可以通过链式调用实现复杂的数据处理管道,但由于中间操作是惰性执行的,只有当终端操作被调用时,流的处理才真正发生。

终端操作

终端操作(Terminal Operations)是那些在流(Stream)上执行最终操作并生成结果(非流)的操作。一旦执行终端操作,流就会被“消耗”,不能再使用。

遍历操作

⨳ forEach()

void forEach(Consumer<? super T> action);

forEach() 可以对流中的每个元素执行指定的操作,通常用于遍历流中的元素。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);

其实中间操作也有一个用于遍历的方法: peek()

⨳ peek()

Stream peek(Consumer<? super T> action);

peek() 用于在流的每个元素上执行一个操作(通常用于调试或日志记录),但不会终止流。

Stream<Integer> stream = Stream.of(1, 2, 3);
Stream<Integer> peekedStream = stream.peek(System.out::println); // 输出每个元素

注意,peek 是一个中间操作,用于在流的处理中“窥视”元素。它允许你在流的处理过程中查看元素的中间状态而不会干扰流的处理流程。通常用于调试目的,以便查看流中的数据流动情况。

收集操作

⨳ collect()

<R, A> R collect(Collector<? super T, A, R> collector);

collect() 可以将流中的元素收集到一个容器(如 ListSetMap)中,通常用于将流的结果转换为集合或其他可变数据结构。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .collect(Collectors.toList());

⨳ toArray()

Object[] toArray();

A[] toArray(IntFunction<A[]> generator);

toArray() 可以将流中的元素收集到一个数组中。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String[] namesArray = names.stream().toArray(String[]::new);

归约操作

⨳ reduce()

Optional reduce(BinaryOperator accumulator);

T reduce(T identity, BinaryOperator accumulator);

reduce() 可以将流中的元素反复结合,归约成单一的值,通常用于求和、求积或连接字符串。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream().reduce(0, Integer::sum);

⨳ count()

long count();

count()用于计算流中元素的数量。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
long count = names.stream().count();

⨳ min()

Optional min(Comparator<? super T> comparator);

min() 可以返回流中最小的元素(按指定的比较器)。

List<Integer> numbers = Arrays.asList(3, 5, 1, 2);
Optional<Integer> min = numbers.stream().min(Integer::compareTo);

⨳ max()

Optional max(Comparator<? super T> comparator);

max() 可以返回流中最大的元素(按指定的比较器)。

List<Integer> numbers = Arrays.asList(3, 5, 1, 2);
Optional<Integer> max = numbers.stream().max(Integer::compareTo);

匹配操作

⨳ findFirst()

Optional findFirst();

findFirst() 可以返回流中的第一个元素,通常用于查找符合条件的第一个元素。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> firstName = names.stream().findFirst();

⨳ findAny()

Optional findAny();

findAny() 可以返回流中的任意一个元素(在并行流的情况下可能更快),通常用于查找任意符合条件的元素。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> anyName = names.stream().findAny();

⨳ anyMatch()

boolean anyMatch(Predicate<? super T> predicate);

anyMatch() 用于判断流中是否有任意元素匹配给定的谓词。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
boolean hasCharlie = names.stream().anyMatch(name -> name.equals("Charlie"));

⨳ allMatch()

boolean anyMatch(Predicate<? super T> predicate);

allMatch() 用于判断流中是否所有元素都匹配给定的谓词。

List<Integer> numbers = Arrays.asList(2, 4, 6, 8);
boolean allEven = numbers.stream().allMatch(num -> num % 2 == 0);

⨳ noneMatch()

boolean noneMatch(Predicate<? super T> predicate);

noneMatch() 用于判断流中是否没有任何元素匹配给定的谓词。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
boolean noneStartsWithD = names.stream().noneMatch(name -> name.startsWith("D"));

这些匹配操作也叫短路操作 (Short-circuiting Operations),匹配到了就不再遍历流了。当然 allMatchnoneMatch 只有在遍历完整个流之后才知道结果。

⨳ ...

这里只列举了几个常用的中间操作和终端操作,更多方法可以到 java.util.stream.Stream 类中寻找,基本上返回值是 Stream 是中间操作,返回值是 非Stream 就是终端操作。

并行流

并行流(Parallel Stream)是 Java Stream API 提供的一种特性,允许在多核处理器上并行处理数据,提升程序的性能,特别是在处理大量数据时。

⨳ 并行流基于 Fork/Join 框架实现,它将任务拆分为子任务,分别在多个线程上并行执行,然后合并结果。这种方式特别适合大数据集的处理。

⨳ 并行流不保证元素的处理顺序,因此,如果操作顺序对结果有影响,可能需要使用 forEachOrdered 等方法。

⨳ 并行流使用公共的 ForkJoinPool,默认的线程数等 Runtime.getRuntime().availableProcessors() 返回的处理器数量。

如果确实需要改变并行流的默认线程池大小,可以通过 ForkJoinPool 构造一个自定义线程池,并在 parallelStream() 方法中指定。

ForkJoinPool customThreadPool = new ForkJoinPool(4);
customThreadPool.submit(() -> list.parallelStream().forEach(System.out::println)).join();

并行流的创建

创建并行流的方式有两种:

通过集合或数组:使用 parallelStream() 方法。

从现有流转换:调用 stream.parallel() 方法。

List<String> list = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 直接创建并行流
Stream<String> parallelStream = list.parallelStream();

// 从现有流转换为并行流
Stream<String> stream = list.stream().parallel();

并行流的代码与顺序流几乎相同,通过一行代码就能将顺序流转换为并行流。

并行流的潜在问题

线程安全问题

并行流操作中如果涉及共享可变状态,可能会导致线程安全问题。避免使用副作用的操作,如修改全局变量、集合等。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = Collections.synchronizedList(new ArrayList<>());
list.parallelStream().forEach(result::add); // 虽然使用了同步集合,但依然存在问题

Collections.synchronizedList 虽然确保了对 result 列表的线程安全,但是 synchronizedList 只是确保了 List 的单个操作是线程安全的。

并行流在内部可能会多线程并发地调用 result::add 方法,但 synchronizedList 只是在操作 List 时进行同步,而没有保证对 List 的遍历和添加操作的线程安全。

顺序依赖

如果操作依赖于元素的处理顺序(如 findFirst),使用并行流可能会导致非预期的行为。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.parallelStream().map(x -> {
    System.out.println(Thread.currentThread().getName() + ": " + x);
    return x * 2;
}).forEach(System.out::println); // 输出顺序不可预测

由于 parallelStream 使用并行处理,map 操作中的 println 语句会在不同的线程中执行。这意味着你会看到线程名称的交错输出,因为 map 操作的每个元素可能由不同的线程处理。

如果需要保证处理顺序,可以使用 forEachOrdered 替代 forEach

性能开销

虽然在多核处理器上,并行流可以显著减少数据处理时间,但因为所有并行流共享同一个 ForkJoinPool,如果在同一时间有多个并行流任务运行,可能导致资源争用和性能下降。

总结

Stream API 提供了一种声明式的方式来处理集合数据,类似于 SQL 查询,支持过滤、映射、排序、规约等操作。

声明式编程 :Stream 提供了一种声明式的编程风格,可以通过链式操作对数据进行处理,而无需明确地控制循环逻辑。例如:

惰性求值 :Stream 中的大多数操作都是惰性执行的(lazy evaluation)。这意味着中间操作(如 filtermap)不会立即执行,只有在执行终端操作(如 collectforEach)时,Stream 才会开始处理数据。这种特性有助于优化性能,避免不必要的计算。

不可变性 : Stream 流是不可变的,每次对流的操作都会生成一个新的 Stream,而不会修改原始数据源。这符合函数式编程的思想,使得代码更加线程安全。

无存储 :Stream 自身不存储数据,它们从数据源(如集合、数组、I/O 通道等)获取数据。因此,Stream 只能被消费一次,使用后就不可再用。

可并行处理 :Stream 提供了简便的并行处理方式。通过 parallelStream() 方法,可以轻松地将流操作并行化,这对于处理大数据集时非常有用,从而充分利用多核处理器的性能。

总而言之,Stream 非常适合处理大数据集、对集合进行复杂操作的场景,它解耦了数据源和操作逻辑,使得开发者可以专注于“要做什么”,而不是“怎么做”。但对于简单的小规模集合操作,传统的循环或迭代器可能更直观和高效。