JavaSE从零开始到精通(七) - Stream流

1. 概述

Java 8引入了Stream API,它提供了一种高效且易于使用的处理集合数据的方式。Stream流可以被认为是一种高级的迭代器,允许我们在集合上进行复杂的操作,例如过滤、映射、排序、归约等,而这些操作可以链式调用,形成流水线。

Stream流采用惰性求值的策略,只有当终端操作被调用时才会执行中间操作,这种特性可以提高性能,避免不必要的计算。

  1. 流的来源

    • 流可以从集合(如List、Set、Map等)、数组、I/O资源等数据源创建。
    • 返回值:Stream
  2. 中间操作

    • 中间操作是流的一部分,允许对流进行转换。它们始终返回一个新的流。例如:过滤、映射、排序、去重等。
    • 返回值:Stream
  3. 终端操作

    • 终端操作是流的最后一步。执行终端操作会触发流的遍历并完成流的处理。终端操作可能会产生一个值或者一个副作用(例如I/O操作)。例如:收集到集合、聚合操作(求和、平均值)、遍历打印等。
    • 返回值:非Stream,一般为基本数据类型。

注意Stream流和管道操作不同:

  • 在操作系统中,管道通常用于进程间通信。管道允许一个进程的输出直接作为另一个进程的输入,从而实现进程间的数据传递。例如 command1 | command2,这将把 command1 的输出作为 command2 的输入。
  • Stream流执行流程:遇到终结操作,就会流水线一样,通过内部迭代,一个一个元素的过滤,不是管道,前者的输出作为后者的输入。

例如:

管道的话:第一次把姓张和姓李的获取出来(张三三,张三,李四四,李四),然后作为输出结果给下一个中间操作。

Stream流:触发终结操作sum(),会执行中间操作,取出张三走流水线,如果符合1,进入2,依次到被其他中间操作筛去,或者被终结操作消费,通过内部迭代依次在处理流中其他数据。

2. 流的特点

  • 不可变性:Stream本身并不存储数据,数据的实际存储可以是集合、数组、文件等,而Stream则提供了便捷和功能强大的API来处理这些数据。它只是提供一种视图或者操作集合数据的流程。

对于每次中间操作,都是返回一个新的流对象,这和字符串非常相似。

  • 延迟执行:流的中间操作可以延迟执行,只有在执行终端操作时才会实际处理数据。

这种特性可以提高性能,避免不必要的计算。

  • 内部迭代:与传统的集合迭代器相比,流使用内部迭代(通过函数式编程方式),开发者不需要显式地管理迭代器或循环。

3. Stream流的获取

1.单列集合的Stream流对象获取

java 复制代码
List<Integer> list = new ArrayList<>();
Collections.addAll(list,2,3,5,7);
Stream<Integer> stream = list.stream();

2.双列集合的Stream流对象获取

java 复制代码
        //创建双列集合对象
        Map<String,String> hash = new HashMap<>();
        //Stream流对象获取: 把双列集合转换成单列集合(根据双列集合获取其键的集合或者是键值对的结合)获取
        Stream<String> s2 = hash.keySet().stream();
        System.out.println(s2);
        Stream<Map.Entry<String, String>> s3 = hash.entrySet().stream();
        System.out.println(s3);

3.数组的Stream流对象获取

java 复制代码
String[] strs = {"张三三","李四四","王五五"};
Stream<String> s4 = Arrays.stream(strs);

4.散装数据的Stream流对象获取

java 复制代码
        //借助于Stream接口的静态方法of,获取散装数据对应的流对象
        Stream<String> s5 = Stream.of("张三三", "李四四", "王五五");

4. Stream流常用方法

4.1 中间方法

中间操作的设计初衷是为了定义对数据流的处理流程,如过滤、映射等,同时保留流的操作链,以便后续操作。

终端操作则负责触发流的实际处理,并生成最终结果或引发副作用,如收集结果或打印输出。

这种分工使得代码结构更加清晰,能够有效地管理和复用流的处理逻辑,同时确保了操作的延迟执行和链式调用的灵活性。

1. filter(Predicate)

  • 根据 Predicate 过滤流中的元素,返回符合条件的元素组成的新 Stream。

Predicate 接口是 Java 中的一个函数式接口(看见函数式接口,不要忘记考虑使用lambda表达式),它定义了一个抽象方法 test,接受一个参数并返回一个 boolean 值。Predicate 主要用于进行条件判断,常见于集合操作和函数式编程中。

boolean test(T t):对给定的输入值 t 进行判断,返回一个 boolean 值。对于Stream流中的filter是返回的为true,表示通过该流进行下一步操作,返回false舍弃元素。

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());

保留偶数到新的集合中,例如: 触发collect终结方法后,1 进入流程,1 % 2 == 1返回false,会被舍弃,2 进入流程 2 % 2 == 0 会返回 true,会被保留,进入collect进行累加到list中。

2. map(Function)

  • 将流中的每个元素映射为另一个元素,通过传入的函数规则进行转换,返回转换后的新 Stream。

Function 是 Java 中的一个函数式接口,定义了一个函数的形式,该函数接受一个参数并产生一个结果。它有一个抽象方法 R apply(T t),接受类型为 T 的参数,返回类型为 R 的结果。

例如:将字符串转换为大写形式。

java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 使用 map 方法将每个字符串转换为大写形式
        List<String> upperCaseNames = names.stream()
                                          .map(String::toUpperCase)
                                          .collect(Collectors.toList());
        
        System.out.println(upperCaseNames);  // 输出 [ALICE, BOB, CHARLIE]
  • 我们定义了一个 Function<String, String> 类型的 toUpperCaseFunction,它使用方法引用 String::toUpperCase
  • upperCaseNames:接受一个字符串作为输入,然后调用这个字符串的 toUpperCase() 方法来生成一个全大写的新字符串。
  • 我们通过 upperCaseNames.apply(input) 将字符串 "Alice" 转换为全大写形式,并将结果进入下一步流,然后终结操作会将其添加到list集合中,依次处理"Bob" 和 "Charlie"。

3. sorted()

  • 对流中的元素进行排序,默认为自然顺序,除非传入自定义的 Comparator。

例如:对流中字符串按照非自然顺序(字符串长度)进行排序。

java 复制代码
    public static void main(String[] args) {
        List<String> words = Arrays.asList("apple", "banana", "pear", "grape", "orange");

        // 自定义一个Comparator,按照字符串长度逆序排序
        Comparator<String> lengthComparator = (str1, str2) -> Integer.compare(str2.length(), str1.length());

        // 使用sorted方法进行排序
        List<String> sortedByLength = words.stream()
                .sorted(lengthComparator)
                .collect(Collectors.toList());

        // 输出排序结果
        System.out.println("Sorted by length (descending): " + sortedByLength);
    }

4. distinct()

  • 去除流中重复的元素,返回由不同元素组成的新 Stream。

例如:对该集合去重,会返回 1 2 3 4 5

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
                                       .distinct()
                                       .collect(Collectors.toList());
  1. limit(long) / skip(long)
  • limit 用于截取流中的前 n 个元素。
  • skip 则用于跳过流中的前 n 个元素。

例如:1.只要前三个元素

2.舍弃前两个元素

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
                                      .limit(3)
                                      .collect(Collectors.toList());

List<Integer> skippedNumbers = numbers.stream()
                                      .skip(2)
                                      .collect(Collectors.toList());

4.2 终结方法

终结方法是对 Stream 执行最终操作并产生最终结果的方法。当调用终结方法时,Stream 才会开始实际的处理过程,同时会关闭 Stream,因此一个 Stream 实例只能调用一次终结方法。终结方法通常会返回一个非 Stream 的结果,例如集合、整数等。

1. forEach(Consumer)

  • 对 Stream 中的每个元素执行 Consumer 指定的操作。

Consumer 是 Java 中的一个函数式接口,用于表示接受单个输入参数并且不返回任何结果的操作。它定义了一个名为 accept 的抽象方法,该方法接受一个参数,但没有返回值。Consumer 接口通常用于需要执行某些操作而不需要返回值的场景。

  • Consumer:提供数据的消费规则的接口
  • accept(数据):accept方法的方法体就是该数据的消费逻辑

这就可以引出**:生产者消费者模型**

  1. 生产者负责生成数据或者放置任务到共享区域(缓冲区)中,以便消费者进程可以访问。
  2. 消费者从共享区域获取数据或任务,并进行相应的处理或消费。
  3. 共享缓存区:生产者和消费者之间共享的数据存储区域。它可以是一个队列、栈或者其他形式的缓冲区。
  4. 生产者将数据放入缓冲区,消费者从缓冲区取出数据。

例如:在函数式编程中,有时我们希望执行某些副作用操作,比如更新状态或打印日志。虽然函数式编程强调无副作用,但有时候还是需要执行这类操作。

java 复制代码
import java.util.stream.Stream;

public class SideEffectExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("apple", "banana", "orange", "pear");

        stream.forEach(item -> {
            System.out.println("Processing item: " + item);
            // 执行一些副作用操作
        });
    }
}
  1. collect(Collectors)
  • 将 Stream 转换为其他形式,接收一个 Collectors 参数,将 Stream 中的元素累积(底层调用不同集合的添加api,例如add ) 到一个可变结果容器中。
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Set<String> nameSet = names.stream()
                          .collect(Collectors.toSet());
  1. reduce(BinaryOperator)
  • 使用给定的函数来组合流中的元素,得到单个值。将流中的元素按照给定的函数来组合,最终得到一个单一的结果。这个函数必须是一个 BinaryOperator,即它接受两个相同类型的参数,并返回一个同类型的结果。

BiFunction 接口是Java中的一个函数式接口,它代表了一个接受两个参数并产生一个结果的函数。它的作用主要是定义了一个可以接受两个参数并返回一个结果的函数的形式,从而可以方便地在函数式编程中使用。

例如:对流中的数据求乘积。

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a * b);

4. count()

  • 返回 Stream 中的元素数量。
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
long count = names.stream()
                  .count();

5. 静态方法

1. 创建流

Stream.of(T... values)

创建一个包含指定元素的流。

java 复制代码
Stream<String> stream = Stream.of("apple", "banana", "orange");

2. 转换流

Stream.concat(Stream<? extends T> a, Stream<? extends T> b)

连接两个流。

java 复制代码
Stream<String> stream1 = Stream.of("apple", "banana");
Stream<String> stream2 = Stream.of("orange", "grape");
Stream<String> concatenatedStream = Stream.concat(stream1, stream2);

3. 生成特定类型的流

IntStream.range(int startInclusive, int endExclusive)

生成一个从 startInclusive 到 endExclusive 的整数流(不包括 endExclusive)。

java 复制代码
IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 4

扩展:为什么要使用函数式接口?为什么不直接传参?

传统的面向对象编程中,我们通常是通过定义方法来传递参数和实现功能。然而,函数式编程的核心思想是将函数作为数据进行处理,这种方式更为灵活和简洁。直接传参的方式虽然依然有效,但在需要复杂的行为组合、条件处理或者并行处理时,使用函数式接口和Lambda表达式能够提供更为清晰和简洁的解决方案。

相关推荐
迷失蒲公英1 小时前
XML与Go结构互转实现(序列化及反序列化)
xml·开发语言·golang
测试盐1 小时前
c++编译过程初识
开发语言·c++
pianmian11 小时前
贪心算法.
算法·贪心算法
赖赖赖先生1 小时前
fastadmin 框架 生成qr code 二维码图片,PHP 7.4版本
开发语言·php
字节流动2 小时前
Android Java 版本的 MSAA OpenGL ES 多重采样
android·java·opengles
玉红7772 小时前
R语言的数据类型
开发语言·后端·golang
夜斗(dou)2 小时前
node.js文件压缩包解析,反馈解析进度,解析后的文件字节正常
开发语言·javascript·node.js
觅远2 小时前
python+PyMuPDF库:(一)创建pdf文件及内容读取和写入
开发语言·python·pdf
chenziang12 小时前
leetcode hot 100 二叉搜索
数据结构·算法·leetcode
呜呼~225143 小时前
前后端数据交互
java·vue.js·spring boot·前端框架·intellij-idea·交互·css3