Java 8 stream的一些用法总结

161 阅读14分钟

1. 概述

在Java 8中,Stream API是一个新添加的功能,它允许在集合上进行更加高效且便捷的操作。Stream API利用内部迭代器,以函数式编程的方式对集合进行处理,可以显著地提高代码的可读性和简洁性。

先看在Java中Stream的定义:

A sequence of elements supporting sequential(顺序) and parallel(并行) aggregate(聚集/汇集) operations.

可以总结为以下两点:

  1. Stream是元素的集合,这点让Stream看起来有些类似Iterator。
  2. 支持顺序和并行的汇聚操作。

Stream流的作用有以下几个方面:

  • 简化集合操作。Stream对集合提供了过滤、映射、排序、聚合等操作,使得集合操作变得容易。
  • 延迟计算。流式操作不保存中间状态,只有在结果时才会进行计算,提升效率。
  • 并行处理。Stream流天然支持并行操作,可以利用多核处理器的性能优势,提高处理速度。
  • 函数式编程风格。函数式编程简化了代码提高了灵活性。

使用流式编程的优点:

  • 声明式编程风格:流式编程采用了一种声明式的编程风格,只需要描述对数据执行的操作,无序显示地编写迭代和控制流语句。即只需要关注结果,不需要在意实现过程,使得代码更加简洁、清晰、直观和易于理解。
  • 链式调用:链式调用允许将多个操作链接在一起,每个方法都返回一个新的流对象,代码执行看起来像流水线一样,使得代码看起来更加清晰、流畅,同时减少了保存中间变量的操作,节省了内存空间。
  • 一系列操作方法:流式编程提供了一系列操作方法,如过滤、映射、排序、聚合等等
  • 减少循环和条件:流式编程替代了传统的循环和条件语句的使用,例如可以使用filter方法进行元素的筛选,使用map方法进行元素的转换,使用reduce方法进行聚合操作。

2. Stream的创建

2.1 从集合创建

直接调用集合的stream()方法创建一个strem流对象

List<String> strs = Arrays.asList("aaa","bbb","ccc");
Stream<String> stream = strs.strem();

2.2 从数组创建

String[] names = {"Alice","BOb","Lucy"};
Stream<String> stream = Arrays.stream(names);

2.3 Stream.of()

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

2.4 Stream.builder()

Stream.builder() 创建一个 Stream.Builder 对象,并使用其 add() 方法来逐个添加元素,最后调用 build() 方法生成 Stream 对象

Stream.Builder<String> builder = Stream.builder();
builder.add("Apple");
builder.add("Banana");
builder.add("Cherry");
Stream<String> stream = builder.build();

2.5 从I/O资源创建

Java 8 引入了一些新的 I/O 类(如 BufferedReaderFiles 等),它们提供了很多方法来读取文件、网络流等数据。这些方法通常返回一个 Stream 对象,可以直接使用

Path path = Paths.get("data.txt");
try (Stream<String> stream = Files.lines(path)) {
    // 使用 stream 处理数据
} catch (IOException e) {
    e.printStackTrace();
}

2.6 通过生成器创建

除了从现有的数据源创建 Stream,我们还可以使用生成器来生成元素。Java 8 中提供了 Stream.generate() 方法和 Stream.iterate() 方法来创建无限 Stream

Stream<Integer> stream = Stream.generate(() -> 0); // 创建一个无限流,每个元素都是 0
Stream<Integer> stream = Stream.iterate(0, n -> n + 1); // 创建一个无限流,从 0 开始递增

3. Stream的中间操作

3.1 过滤filter

filter() 方法接受一个 Predicate 函数作为参数,用于过滤 Stream 中的元素。只有满足 Predicate 条件的元素会被保留下来

Steam<Integer> stream = Stream.of(1,2,3,4,5,6,7,8);
Stream<Integer> filterStream = stream.filter(n -> n%2 ==0);

关于Predicate函数 Predicate是函数式接口,可以使用Lambda表达式作为参数。

3.2 映射map

map() 方法接受一个 Function 函数作为参数,用于对 Stream 中的元素进行映射转换。对每个元素应用函数后的结果会构成一个新的 Stream

Stream<String> stream = Stream.of("apple", "banana", "cherry");
Stream<Integer> mappedStream = stream.map(s -> s.length()); // 映射为单词长度
// 输出结果: 5 6 6

flatMap() 方法类似于 map() 方法,不同之处在于它可以将每个元素映射为一个流,并将所有流连接成一个流。这主要用于解决嵌套集合的情况。

List<List<Integer>> nestedList = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4),
    Arrays.asList(5, 6)
);
Stream<Integer> flattenedStream = nestedList.stream().flatMap(List::stream); // 扁平化为一个流

3.3 排序sorted

排序操作(sorted)是 Stream API 中的一种常用操作方法,它用于对 Stream 中的元素进行排序。排序操作可以按照自然顺序或者使用自定义的比较器进行排序.

Stream<String> stream = Stream.of("banana", "apple", "cherry");
Stream<String> sortedStream = stream.sorted();
sortedStream.forEach(System.out::println); // 输出结果: apple banana cherry

3.4 截断 limit和skip

截断操作(limit和skip)是 Stream API 中常用的操作方法,用于在处理流的过程中对元素进行截断。

  • limit(n):保留流中的前n个元素,返回一个包含最多n个元素的新流。如果流中元素少于n个,则返回原始流
  • skip(n):跳过流中的前n个元素,返回一个包含剩余元素的新流。如果流中元素少于n个,则返回一个空流。
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> limitedStream = stream.limit(3);
limitedStream.forEach(System.out::println); // 输出结果: 1 2 3

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> skippedStream = stream.skip(2);
skippedStream.forEach(System.out::println); // 输出结果: 3 4 5

4. Stream的终端操作

4.1 forEach

forEach和peek都是Stream API中用于遍历流中元素的操作方法,它们在处理流的过程中提供了不同的功能和使用场景。

forEach: forEach是一个终端操作方法,它接受一个Consumer函数作为参数,对流中的每个元素执行该函数。它没有返回值,因此无法将操作结果传递给后续操作。forEach会遍历整个流,对每个元素执行相同的操作。

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

这里提一下peek操作,peek也可以进行遍历,但和foreach稍有区别。 peek是一个中间操作方法,它接受一个Consumer函数作为参数,对流中的每个元素执行该函数。与forEach不同的是,peek方法会返回一个新的流,该流中的元素和原始流中的元素相同。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)//转换大写
                                   .peek(System.out::println)
                                   .collect(Collectors.toList());
//使用peek在中间打印元素                                  

需要注意的是,无论是forEach还是peek,它们都是用于在流的处理过程中执行操作。区别在于forEach是终端操作,不返回任何结果,而peek是中间操作,可以和其他操作方法进行组合和链式调用。

根据使用场景和需求,选择使用forEach或peek来遍历流中的元素。如果只是需要遍历输出元素,不需要操作结果,则使用forEach。如果需要在遍历过程中执行一些其他操作,并将元素传递给后续操作,则使用peek。

4.2 聚合操作reduce和collect

reduce和collect都是Stream API中用于聚合操作的方法,它们可以将流中的元素进行汇总、计算和收集。

reduce是一个终端操作方法,它接受一个BinaryOperator函数作为参数,对流中的元素逐个进行合并操作,最终得到一个结果。该方法会将流中的第一个元素作为初始值,然后将初始值与下一个元素传递给BinaryOperator函数进行计算,得到的结果再与下一个元素进行计算,以此类推,直到遍历完所有元素。

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

collect是一个终端操作方法,它接受一个Collector接口的实现作为参数,对流中的元素进行收集和汇总的操作。Collector接口定义了一系列用于聚合操作的方法,例如收集元素到List、Set、Map等容器中,或进行字符串连接、分组、计数等操作

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String joinedNames = names.stream()
                          .collect(Collectors.joining(", "));
System.out.println(joinedNames); 
// 将list通过“, ”拼接,输出结果: Alice, Bob, Charlie

在这个示例中,我们创建了一个包含字符串的List,并通过stream()方法将其转换为流。然后使用collect方法将流中的元素连接成一个字符串,每个元素之间使用逗号和空格分隔。

需要注意的是,reduce和collect都是终端操作,它们都会触发流的遍历和处理。不同的是,reduce方法用于对流中的元素进行累积计算,得到一个最终结果;而collect方法用于对流中的元素进行收集和汇总,得到一个容器或其他自定义的结果。

在选择使用reduce还是collect时,可以根据具体需求和操作类型来决定。如果需要对流中的元素进行某种计算和合并操作,得到一个结果,则使用reduce。如果需要将流中的元素收集到一个容器中,进行汇总、分组、计数等操作,则使用collect。

4.3 匹配操作allMatch/anyMatch/noneMatch

在 Stream API 中,allMatch、anyMatch 和 noneMatch 是用于进行匹配操作的方法,它们可以用来检查流中的元素是否满足特定的条件。

  • allMatch 方法用于判断流中的所有元素是否都满足给定的条件。当流中的所有元素都满足条件时,返回 true;如果存在一个元素不满足条件,则返回 false
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allEven = numbers.stream()
                         .allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 输出结果: false
  • anyMatch 方法用于判断流中是否存在至少一个元素满足给定的条件。当流中至少有一个元素满足条件时,返回 true;如果没有元素满足条件,则返回 false。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream()
                         .anyMatch(n -> n % 2 == 0);
System.out.println(hasEven); // 只要有一个满足就为true,输出结果: true
  • noneMatch 方法用于判断流中的所有元素是否都不满足给定的条件。当流中没有元素满足条件时,返回 true;如果存在一个元素满足条件,则返回 false
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean noneNegative = numbers.stream()
                             .noneMatch(n -> n < 0);
System.out.println(noneNegative); // 输出结果: true

需要注意的是,allMatch、anyMatch 和 noneMatch 都是终端操作,它们会遍历流中的元素直到满足条件或处理完所有元素。在性能上,allMatch 和 noneMatch 在第一个不匹配的元素处可以立即返回结果,而 anyMatch 在找到第一个匹配的元素时就可以返回结果

4.4 查找操作findFirst/findAny

在 Stream API 中,findFirst 和 findAny 是用于查找操作的方法,它们可以用来从流中获取满足特定条件的元素。

  • findFirst 方法用于返回流中的第一个元素。它返回一个 Optional 对象,如果流为空,则返回一个空的 Optional;如果流非空,则返回流中的第一个元素的 Optional。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> first = names.stream()
                              .findFirst();
first.ifPresent(System.out::println); //ifPresent 方法判断 Optional 是否包含值, 输出结果: Alice
  • findAny 方法用于返回流中的任意一个元素。它返回一个 Optional 对象,如果流为空,则返回一个空的 Optional;如果流非空,则返回流中的任意一个元素的 Optional。在顺序流中,通常会返回第一个元素;而在并行流中,由于多线程的处理,可能返回不同的元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> any = numbers.stream()
                               .filter(n -> n % 2 == 0)
                               .findAny();
any.ifPresent(System.out::println); // 输出结果: 2 或 4(取决于并行处理的结果)

需要注意的是,findAny 在并行流中会更有优势,因为在多线程处理时,可以返回最先找到的元素,提高效率。而在顺序流中,findAny 的性能与 findFirst 相当

4.5 统计操作couunt/max/min

在 Stream API 中,count、max 和 min 是用于统计操作的方法,它们可以用来获取流中元素的数量、最大值和最小值

  • count 方法用于返回流中元素的数量。它返回一个 long 类型的值,表示流中的元素个数
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream()
                    .count();
System.out.println(count); // 输出结果: 5
  • max 方法用于返回流中的最大值。它返回一个 Optional 对象,如果流为空,则返回一个空的 Optional;如果流非空,则返回流中的最大值的 Optional
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream()
                               .max(Integer::compareTo);
max.ifPresent(System.out::println); // 输出结果: 5
  • min 方法用于返回流中的最小值。它返回一个 Optional 对象,如果流为空,则返回一个空的 Optional;如果流非空,则返回流中的最小值的 Optional。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> min = numbers.stream()
                               .min(Integer::compareTo);
min.ifPresent(System.out::println); // 输出结果: 1

这些统计操作方法提供了一种便捷的方式来对流中的元素进行数量、最大值和最小值的计算。通过返回 Optional 对象,可以避免空指针异常。

5. Stream并行操作

并行流是 Java 8 Stream API 中的一个特性。它可以将一个流的操作在多个线程上并行执行,以提高处理大量数据时的性能。

在传统的顺序流中,所有的操作都是在单个线程上按照顺序执行的。而并行流则会将流的元素分成多个小块,并在多个线程上并行处理这些小块,最后将结果合并起来。这样可以充分利用多核处理器的优势,加快数据处理的速度。

要将一个顺序流转换为并行流,只需调用流的 parallel() 方法即可。

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

在这个示例中,我们创建了一个包含整数的 List,并通过 stream() 方法将其转换为流。接着调用 parallel() 方法将流转换为并行流,然后使用 forEach 方法遍历流中的元素并输出。

需要注意的是,并行流的使用并不总是适合所有情况。并行流的优势主要体现在数据量较大、处理时间较长的场景下。对于小规模数据和简单的操作,顺序流可能更加高效。在选择使用并行流时,需要根据具体情况进行评估和测试,以确保获得最佳的性能。

此外,还需要注意并行流在某些情况下可能引入线程安全的问题。如果多个线程同时访问共享的可变状态,可能会导致数据竞争和不确定的结果。因此,在处理并行流时,应当避免共享可变状态,或采用适当的同步措施来确保线程安全。

使用并行流可以通过利用多线程并行处理数据,从而提高程序的执行性能。下面是一些使用并行流提高性能的常见方法:

5.1 并行流的使用

  1. 创建并行流:要创建一个并行流,只需在普通流上调用 parallel() 方法。

    java
    复制代码
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> parallelStream = numbers.parallelStream();
    
  • 利用任务并行性:并行流会将数据分成多个小块,并在多个线程上并行处理这些小块。这样可以充分利用多核处理器的优势。

    java
    复制代码
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.parallelStream()
           .map(n -> compute(n)) // 在多个线程上并行处理计算
           .forEach(System.out::println);
    

    在这个示例中,使用 map 方法对流中的每个元素进行计算。由于并行流的特性,计算操作会在多个线程上并行执行,提高了计算的效率。

  • 避免共享可变状态:在并行流中,多个线程会同时操作数据。如果共享可变状态(如全局变量)可能导致数据竞争和不确定的结果。因此,避免在并行流中使用共享可变状态,或者采取适当的同步措施来确保线程安全。

  • 使用合适的操作:一些操作在并行流中的性能表现更好,而另一些操作则可能导致性能下降。一般来说,在并行流中使用基于聚合的操作(如 reducecollect)和无状态转换操作(如 mapfilter)的性能较好,而有状态转换操作(如 sorted)可能会导致性能下降。

    java
    复制代码
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
    // good performance
    int sum = numbers.parallelStream()
                     .reduce(0, Integer::sum);
    
    // good performance
    List<Integer> evenNumbers = numbers.parallelStream()
                                       .filter(n -> n % 2 == 0)
                                       .collect(Collectors.toList());
    
    // potential performance degradation
    List<Integer> sortedNumbers = numbers.parallelStream()
                                         .sorted()
                                         .collect(Collectors.toList());
    

    在这个示例中,reducefilter 的操作在并行流中具有良好的性能,而 sorted 操作可能导致性能下降。

除了上述方法,还应根据具体情况进行评估和测试,并行流是否能够提高性能。有时候,并行流的开销(如线程的创建和销毁、数据切割和合并等)可能超过了其带来的性能提升。因此,在选择使用并行流时,应该根据数据量和操作复杂度等因素进行综合考虑,以确保获得最佳的性能提升。