前端视角 Java Web 入门手册 2.7:Java Core ——Stream API

194 阅读5分钟

Stream API 是 Java 8 引入的一种用于处理集合数据的抽象,它代表了一组元素,可以是集合、数组或其它数据源中的元素。Stream API 允许开发者以一种类似于管道的方式对这些元素进行一系列的操作,如过滤、映射、排序、归约等,而无需显式地使用循环和临时变量

Stream 并不是数据结构,也不是线程安全的,它更像是对数据源(如集合、数组等)的某种查询接口。

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
fruits.stream()
      .filter(fruit -> fruit.startsWith("A"))
      .forEach(System.out::println); // 输出: Apple

Stream 核心特性

  • 不可变性:Stream 本身是不可变的,即 Stream 中的元素不能被直接修改。如果需要对元素进行修改,通常需要通过映射等操作创建一个新的 Stream。
  • 不可复用:一旦执行了终端操作,Stream 就会被消耗掉,不能再进行后续的操作。如果需要再次对相同的数据进行操作,需要重新获取 Stream。
  • 延迟执行:Stream 的中间操作是延迟执行的,只有在终端操作触发时,整个流才会被执行

创建 Stream

Stream 可以从多种数据源创建,如集合、数组、I/O 通道等

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamCreationExample {
    public static void main(String[] args) {
        // 从集合创建 Stream
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        Stream<String> streamFromList = names.stream();

        // 从数组创建 Stream
        String[] nameArray = {"David", "Eve", "Frank"};
        Stream<String> streamFromArray = Arrays.stream(nameArray);

        // 使用 Stream.of() 方法
        Stream<String> streamOf = Stream.of("Grace", "Heidi", "Ivan");

        // 打印所有流
        streamFromList.forEach(System.out::println);
        streamFromArray.forEach(System.out::println);
        streamOf.forEach(System.out::println);
    }
}

Stream 常用操作

Stream API 提供了丰富的操作,可以分为两类

  1. 中间操作(Intermediate Operations):这类操作会返回一个新的 Stream,并且可以在一个 Stream 上连续调用多个中间操作,形成操作链
  2. 终端操作(Terminal Operations):执行 Stream 操作链,并返回一个结果或产生某种副作用(如打印输出)。一旦执行了终端操作,Stream 就会被消耗掉,不能再进行后续的操作

中间操作

filter

根据指定条件过滤元素,保留满足条件的元素

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
fruits.stream()
      .filter(fruit -> fruit.startsWith("A"))
      .forEach(System.out::println); // 输出: Apple

map

将每个元素映射为另一种形式或类型

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
fruits.stream()
      .map(String::toUpperCase)
      .forEach(System.out::println); 
// 输出: APPLE, BANANA, CHERRY

sorted

对元素进行排序,按自然顺序或指定的比较器顺序

List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
numbers.stream()
       .sorted()
       .forEach(System.out::println); 
// 输出: 1, 1, 3, 4, 5, 9

distinct

去除重复元素,保留唯一的元素

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
numbers.stream()
       .distinct()
       .forEach(System.out::println); 
// 输出: 1, 2, 3, 4, 5

limit

限制 Stream 的元素数量,只保留前 N 个元素

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .limit(3)
       .forEach(System.out::println); 
// 输出: 1, 2, 3

skip

跳过前 N 个元素,保留剩余的元素

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .skip(2)
       .forEach(System.out::println); 
// 输出: 3, 4, 5

终端操作

forEach

对 Stream 的每个元素执行指定的动作

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
fruits.stream()
      .forEach(System.out::println);
// 输出: Apple, Banana, Cherry

collect

将 Stream 的元素收集到集合、列表、映射等容器中

import java.util.List;
import java.util.stream.Collectors;

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
List<String> filteredFruits = fruits.stream()
                                    .filter(fruit -> fruit.startsWith("B"))
                                    .collect(Collectors.toList());

System.out.println(filteredFruits); // 输出: [Banana]

在 Java 16 之前将 Stream 中的元素收集到一个 List 中时,需要使用 .collect(Collectors.toList())

从 Java 16 开始,Stream 接口本身新增了 toList() 方法,这样就可以直接使用该方法来将流中的元素收集到一个不可变的 List 里,代码会更加简洁

import java.util.List;

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
List<String> filteredFruits = fruits.stream()
                                    .filter(fruit -> fruit.startsWith("B"))
                                    .toList();

System.out.println(filteredFruits); // 输出: [Banana]

reduce

通过指定的操作将 Stream 的元素归约为单一的结果

import java.util.Optional;

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a + b);

sum.ifPresent(System.out::println); // 输出: 15

count

计算 Stream 中元素的数量

long count = Stream.of("Apple", "Banana", "Cherry")
                   .count();

System.out.println(count); // 输出: 3

anyMatch, allMatch, noneMatch

判断 Stream 中是否存在满足特定条件的元素

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");

// 是否存在以 'A' 开头的元素
boolean anyStartsWithA = fruits.stream()
                               .anyMatch(fruit -> fruit.startsWith("A"));
System.out.println(anyStartsWithA); // 输出: true

// 是否所有元素长度大于 5
boolean allLongerThan5 = fruits.stream()
                                .allMatch(fruit -> fruit.length() > 5);
System.out.println(allLongerThan5); // 输出: false

// 是否不存在包含 'Z' 的元素
boolean noneContainZ = fruits.stream()
                             .noneMatch(fruit -> fruit.contains("Z"));
System.out.println(noneContainZ); // 输出: true

findFirst, findAny

返回 Stream 中第一个元素或任意一个元素,通常与并行流结合使用

import java.util.Optional;

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");

// 找到第一个以 'C' 开头的元素
Optional<String> firstC = fruits.stream()
                                .filter(fruit -> fruit.startsWith("C"))
                                .findFirst();
firstC.ifPresent(System.out::println); // 输出: Cherry

// 并行流中的 findAny
Optional<String> any = fruits.parallelStream()
                             .filter(fruit -> fruit.startsWith("B"))
                             .findAny();
any.ifPresent(System.out::println); // 输出: Banana

并行流

并行流是一种特殊的流,它可以将流中的元素分成多个块,每个块由不同的线程并行处理。Java 会自动管理线程的创建、调度和销毁,使得开发者可以更方便地利用多核处理器的计算能力,提高数据处理的效率

将普通流转换为并行流非常简单,只需要调用 parallelStream() 方法即可

import java.util.List;
import java.util.Random;

public class ParallelStreamPerformanceExample {
    public static void main(String[] args) {
        // 生成一百万个随机数
        List<Integer> randomNumbers = new Random().ints(1, 100)
                                               .limit(1_000_000)
                                               .boxed()
                                               .toList();

        // 顺序流计算平方和
        long start = System.currentTimeMillis();
        long sumSequential = randomNumbers.stream()
                                         .mapToLong(n -> n * n)
                                         .sum();
        long end = System.currentTimeMillis();
        System.out.println("顺序流平方和: " + sumSequential + ",耗时: " + (end - start) + " ms");

        // 并行流计算平方和
        start = System.currentTimeMillis();
        long sumParallel = randomNumbers.parallelStream()
                                       .mapToLong(n -> n * n)
                                       .sum();
        end = System.currentTimeMillis();
        System.out.println("并行流平方和: " + sumParallel + ",耗时: " + (end - start) + " ms");
    }
}

输出结果

顺序流平方和: 3318884152,耗时: 5 ms
并行流平方和: 3318884152,耗时: 12 ms

并行流中的操作可能会在多个线程中同时执行,因此在处理共享资源时需要确保线程安全。如果在并行流中使用了非线程安全的对象,可能会导致数据不一致或其它并发问题

import java.util.ArrayList;
import java.util.List;

public class ParallelStreamThreadSafety {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        List<Integer> result = new ArrayList<>();

        // 这是不安全的,因为 ArrayList 不是线程安全的
        numbers.parallelStream()
               .forEach(result::add);

        System.out.println(result);
    }
}

在上述代码中,ArrayList 不是线程安全的,使用并行流的 forEach 方法向其中添加元素可能会导致并发修改异常。可以使用线程安全的集合,如 CopyOnWriteArrayList 来避免这个问题

Optional 与 Stream

Optional 是 Java 8 引入的一个容器对象,用于防止出现 NullPointerException。它经常与 Stream 结合使用,特别是在处理可能为空的结果时

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

// 查找第一个以 'D' 开头的名称
Optional<String> optionalName = names.stream()
                                     .filter(name -> name.startsWith("D"))
                                     .findFirst();

optionalName.ifPresent(name -> System.out.println("Found: " + name));
// 如果没有找到,则不会输出