Java 8 `Stream API`:函数式编程与集合操作的性能优化

Java 8 Stream API:函数式编程与集合操作的性能优化——告别笨重循环,拥抱丝滑流畅!

各位亲爱的程序员们,大家撸码辛苦啦!有没有觉得每天对着那些冗长的for循环、if-else判断,头都大了几圈?是不是经常梦想着能用一种更优雅、更高效的方式来处理集合数据? 别着急,Java 8 的 Stream API 就是来拯救你们的!

今天,我们就来好好聊聊这个神奇的 Stream API, 看看它如何用函数式编程的思想,为我们的集合操作带来性能上的飞跃,让我们告别笨重的循环,拥抱丝滑流畅的代码体验!

一、 什么是 Stream API? 你以为的河流,其实是数据管道!

想象一下,你站在一条缓缓流淌的河流旁,河里漂浮着各种各样的东西:树叶、小鱼、塑料瓶……你想把这些东西过滤一下,只留下树叶。传统的方式,你可能需要拿着网兜,一个个捞出来,然后判断是不是树叶。

Stream API 就像一条数据管道,你只需要告诉管道你需要什么,它就会自动把符合条件的东西筛选出来,然后送到你手里。这个管道可以进行各种各样的处理,比如过滤、排序、转换等等,而且这些处理都是并行进行的,速度飞快!

简单来说,Stream API 是 Java 8 引入的一套用于处理集合数据的 API。它基于函数式编程的思想,允许你以声明式的方式操作集合,而无需编写大量的循环代码。

二、 为什么我们需要 Stream API? 代码更优雅,性能更卓越!

传统的集合操作方式,通常使用循环来实现,代码冗长,可读性差,而且容易出错。更重要的是,循环是顺序执行的,无法充分利用多核 CPU 的优势。

Stream API 的出现,解决了这些问题。它具有以下优点:

  • 代码简洁: 使用 Stream API 可以用更少的代码完成复杂的集合操作。
  • 可读性强: Stream API 使用链式调用,代码逻辑清晰易懂。
  • 并行处理: Stream API 可以自动将操作并行化,充分利用多核 CPU 的优势,提高性能。
  • 延迟执行: Stream API 的很多操作都是延迟执行的,只有在需要结果时才会执行,避免了不必要的计算。

举个例子,假设我们要从一个列表中筛选出所有大于 10 的偶数,并计算它们的平方和。

传统方式:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);

List<Integer> evenNumbersGreaterThanTen = new ArrayList<>();
for (Integer number : numbers) {
    if (number > 10 && number % 2 == 0) {
        evenNumbersGreaterThanTen.add(number);
    }
}

List<Integer> squaredNumbers = new ArrayList<>();
for (Integer number : evenNumbersGreaterThanTen) {
    squaredNumbers.add(number * number);
}

int sum = 0;
for (Integer number : squaredNumbers) {
    sum += number;
}

System.out.println("Sum of squares of even numbers greater than 10: " + sum);

Stream API 方式:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);

int sum = numbers.stream()
                .filter(n -> n > 10 && n % 2 == 0)
                .map(n -> n * n)
                .reduce(0, Integer::sum);

System.out.println("Sum of squares of even numbers greater than 10: " + sum);

看到区别了吗? Stream API 的代码更加简洁、易读,而且性能更好。

三、 Stream API 的核心概念: 掌握这些,你就是 Stream 大师!

Stream API 的核心概念主要有三个:

  • Stream: 代表一个元素序列,可以来自集合、数组、I/O 管道等等。
  • 中间操作: 对 Stream 进行转换的操作,例如 filtermapsorted 等。中间操作会返回一个新的 Stream。
  • 终端操作: 产生结果或副作用的操作,例如 forEachcollectreduce 等。终端操作会消耗 Stream,执行完毕后 Stream 就不能再使用了。

可以用下图简单概括:

[数据源] --> [Stream] --> [中间操作1] --> [Stream] --> [中间操作2] --> [Stream] --> ... --> [终端操作] --> [结果]

1. Stream 的创建

创建 Stream 的方式有很多种:

  • 从集合创建:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> stream = numbers.stream();  // 顺序流
    Stream<Integer> parallelStream = numbers.parallelStream(); // 并行流
  • 从数组创建:

    int[] numbers = {1, 2, 3, 4, 5};
    IntStream stream = Arrays.stream(numbers);
  • 使用 Stream.of() 创建:

    Stream<String> stream = Stream.of("a", "b", "c");
  • 使用 Stream.iterate() 创建无限流:

    Stream<Integer> stream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
  • 使用 Stream.generate() 创建无限流:

    Stream<Double> stream = Stream.generate(Math::random); // 随机数流
  • 从文件创建:

    try (Stream<String> stream = Files.lines(Paths.get("file.txt"))) {
        // 处理文件中的每一行
    } catch (IOException e) {
        e.printStackTrace();
    }

2. 常见的中间操作

  • filter(Predicate<T> predicate) 过滤 Stream 中的元素,只保留符合条件的元素。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    numbers.stream()
            .filter(n -> n % 2 == 0) // 过滤出偶数
            .forEach(System.out::println); // 输出 2, 4, 6, 8, 10
  • map(Function<T, R> mapper) 将 Stream 中的每个元素转换为另一种类型。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    names.stream()
         .map(String::toUpperCase) // 将字符串转换为大写
         .forEach(System.out::println); // 输出 ALICE, BOB, CHARLIE
  • flatMap(Function<T, Stream<R>> mapper) 将 Stream 中的每个元素转换为一个 Stream,然后将所有 Stream 合并成一个 Stream。

    List<List<Integer>> numbers = Arrays.asList(
            Arrays.asList(1, 2),
            Arrays.asList(3, 4),
            Arrays.asList(5, 6)
    );
    
    numbers.stream()
           .flatMap(List::stream) // 将 List<List<Integer>> 转换为 Stream<Integer>
           .forEach(System.out::println); // 输出 1, 2, 3, 4, 5, 6
  • distinct() 去除 Stream 中重复的元素。

    List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
    numbers.stream()
           .distinct() // 去除重复元素
           .forEach(System.out::println); // 输出 1, 2, 3, 4
  • sorted() 对 Stream 中的元素进行排序。

    List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4);
    numbers.stream()
           .sorted() // 升序排序
           .forEach(System.out::println); // 输出 1, 2, 4, 5, 8, 9
    
    numbers.stream()
           .sorted(Comparator.reverseOrder()) // 降序排序
           .forEach(System.out::println); // 输出 9, 8, 5, 4, 2, 1
  • peek(Consumer<T> action) 对 Stream 中的每个元素执行一些操作,但不改变 Stream 本身。通常用于调试。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.stream()
           .peek(System.out::println) // 输出每个元素
           .filter(n -> n % 2 == 0)
           .forEach(System.out::println); // 只输出偶数,但之前会先输出所有元素
  • limit(long maxSize) 截取 Stream 中的前 maxSize 个元素。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    numbers.stream()
           .limit(5) // 只取前 5 个元素
           .forEach(System.out::println); // 输出 1, 2, 3, 4, 5
  • skip(long n) 跳过 Stream 中的前 n 个元素。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    numbers.stream()
           .skip(5) // 跳过前 5 个元素
           .forEach(System.out::println); // 输出 6, 7, 8, 9, 10

3. 常见的终端操作

  • forEach(Consumer<T> action) 对 Stream 中的每个元素执行一些操作。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    names.stream()
         .forEach(System.out::println); // 输出 Alice, Bob, Charlie
  • toArray() 将 Stream 中的元素转换为数组。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Integer[] array = numbers.stream()
                              .toArray(Integer[]::new);
    System.out.println(Arrays.toString(array)); // 输出 [1, 2, 3, 4, 5]
  • collect(Collector<T, A, R> collector) 将 Stream 中的元素收集到集合中。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> evenNumbers = numbers.stream()
                                       .filter(n -> n % 2 == 0)
                                       .collect(Collectors.toList()); // 收集到 List 中
    System.out.println(evenNumbers); // 输出 [2, 4]
    
    Set<Integer> oddNumbers = numbers.stream()
                                      .filter(n -> n % 2 != 0)
                                      .collect(Collectors.toSet()); // 收集到 Set 中
    System.out.println(oddNumbers); // 输出 [1, 3, 5]
    
    String joinedString = names.stream()
                               .collect(Collectors.joining(", ")); // 使用逗号连接字符串
    System.out.println(joinedString); // 输出 Alice, Bob, Charlie
    
    double average = numbers.stream()
                         .collect(Collectors.averagingInt(Integer::intValue)); // 计算平均值
    System.out.println(average); // 输出 3.0
  • reduce(T identity, BinaryOperator<T> accumulator) 将 Stream 中的元素归约成一个值。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    int sum = numbers.stream()
                   .reduce(0, Integer::sum); // 计算总和
    System.out.println(sum); // 输出 15
    
    Optional<Integer> max = numbers.stream()
                                  .reduce(Integer::max); // 找出最大值
    System.out.println(max.get()); // 输出 5
  • min(Comparator<T> comparator) 找出 Stream 中的最小值。

    List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4);
    Optional<Integer> min = numbers.stream()
                                  .min(Integer::compareTo); // 找出最小值
    System.out.println(min.get()); // 输出 1
  • max(Comparator<T> comparator) 找出 Stream 中的最大值。

    List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4);
    Optional<Integer> max = numbers.stream()
                                  .max(Integer::compareTo); // 找出最大值
    System.out.println(max.get()); // 输出 9
  • count() 统计 Stream 中元素的个数。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    long count = numbers.stream()
                       .count(); // 统计元素个数
    System.out.println(count); // 输出 5
  • anyMatch(Predicate<T> predicate) 判断 Stream 中是否至少有一个元素符合条件。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    boolean hasEvenNumber = numbers.stream()
                                   .anyMatch(n -> n % 2 == 0); // 是否有偶数
    System.out.println(hasEvenNumber); // 输出 true
  • allMatch(Predicate<T> predicate) 判断 Stream 中是否所有元素都符合条件。

    List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
    boolean allEven = numbers.stream()
                            .allMatch(n -> n % 2 == 0); // 是否所有元素都是偶数
    System.out.println(allEven); // 输出 true
  • noneMatch(Predicate<T> predicate) 判断 Stream 中是否没有元素符合条件。

    List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9);
    boolean noEven = numbers.stream()
                           .noneMatch(n -> n % 2 == 0); // 是否没有偶数
    System.out.println(noEven); // 输出 true
  • findFirst() 找出 Stream 中的第一个元素。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> first = numbers.stream()
                                    .findFirst(); // 找出第一个元素
    System.out.println(first.get()); // 输出 1
  • findAny() 找出 Stream 中的任意一个元素。在并行流中,findAny() 的性能可能比 findFirst() 更好。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> any = numbers.stream()
                                  .findAny(); // 找出任意一个元素
    System.out.println(any.get()); // 输出 1 (也可能是其他元素,取决于具体实现)

四、 Stream API 的性能优化: 并行流的正确打开方式!

Stream API 的一个重要优势是可以进行并行处理,充分利用多核 CPU 的优势,提高性能。但是,使用并行流需要注意一些问题,否则可能会适得其反。

1. 什么时候使用并行流?

  • 数据量大: 当数据量足够大时,并行处理才能体现出优势。
  • 计算密集型: 当操作比较耗时,例如复杂的计算或 I/O 操作时,并行处理才能带来明显的性能提升。
  • 无状态操作: 并行流更适合无状态操作,即操作的结果不依赖于之前的状态。例如 filtermap 等。

2. 什么时候避免使用并行流?

  • 数据量小: 当数据量很小时,并行处理的开销可能会超过收益。
  • 有状态操作: 有状态操作,例如 sorteddistinct 等,在并行处理时需要额外的同步开销,可能会降低性能。
  • I/O 密集型: 如果操作主要是 I/O 操作,并行处理可能不会带来明显的性能提升,甚至可能降低性能。
  • 共享可变状态: 如果多个线程同时访问和修改共享的可变状态,可能会导致数据竞争和死锁。

3. 如何正确使用并行流?

  • 使用 parallelStream() 创建并行流:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.parallelStream() // 创建并行流
           .filter(n -> n % 2 == 0)
           .forEach(System.out::println);
  • 使用 stream().parallel() 将顺序流转换为并行流:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.stream()
           .parallel() // 转换为并行流
           .filter(n -> n % 2 == 0)
           .forEach(System.out::println);
  • 避免共享可变状态: 尽量避免在并行流中使用共享的可变状态。如果必须使用,需要使用适当的同步机制来保证线程安全。

  • 使用 Spliterator 自定义数据分割: Spliterator 允许你自定义如何将数据分割成多个部分,以便并行处理。

4. 性能测试和分析

在使用并行流之前,一定要进行性能测试和分析,以确保它能够带来实际的性能提升。可以使用 JMH (Java Microbenchmark Harness) 等工具进行性能测试。

示例:使用并行流计算 1 到 1000000 的总和

import java.util.stream.IntStream;

public class ParallelStreamExample {

    public static void main(String[] args) {
        // 顺序流
        long startTime = System.nanoTime();
        int sumSequential = IntStream.rangeClosed(1, 1000000)
                                     .sum();
        long endTime = System.nanoTime();
        System.out.println("Sequential sum: " + sumSequential + ", time: " + (endTime - startTime) / 1000000 + "ms");

        // 并行流
        startTime = System.nanoTime();
        int sumParallel = IntStream.rangeClosed(1, 1000000)
                                   .parallel()
                                   .sum();
        endTime = System.nanoTime();
        System.out.println("Parallel sum: " + sumParallel + ", time: " + (endTime - startTime) / 1000000 + "ms");
    }
}

在多核 CPU 的机器上运行这个程序,你会发现并行流的性能明显优于顺序流。

五、 Stream API 的一些高级用法: 进阶之路,永无止境!

除了上面介绍的基本用法,Stream API 还有一些高级用法,可以帮助你解决更复杂的问题。

  • 自定义 Collector: 如果你需要将 Stream 中的元素收集到自定义的集合中,可以使用 Collector.of() 方法创建一个自定义的 Collector。

  • 使用 groupingBy() 进行分组: Collectors.groupingBy() 方法可以将 Stream 中的元素按照某个属性进行分组。

  • 使用 partitioningBy() 进行分区: Collectors.partitioningBy() 方法可以将 Stream 中的元素按照某个条件分成两组。

  • 处理 Optional 值: Stream API 可以很好地处理 Optional 值,避免空指针异常。

六、 总结: Stream API,你值得拥有!

Java 8 Stream API 是一种强大而灵活的工具,可以帮助你更优雅、更高效地处理集合数据。它基于函数式编程的思想,代码简洁、可读性强,而且可以进行并行处理,提高性能。

掌握 Stream API, 你就能告别笨重的循环,拥抱丝滑流畅的代码体验,成为一个更优秀的程序员!

希望这篇文章能够帮助你更好地理解和使用 Stream API。 记住,学习是一个不断探索的过程, 实践才是检验真理的唯一标准。 赶紧动手试试吧, 相信你会爱上 Stream API 的!

码字不易,如果觉得这篇文章对你有帮助, 欢迎点赞、收藏、分享! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注