Java:一篇讲透Stream API(爆肝两万字)

文章目录

Stream API 详解

Java 8 引入的 Stream API 是函数式编程的核心之一,它通过高效的流操作,使得对集合、数组等数据源的处理更加简洁、灵活且声明式。Stream API 能够支持各种类型的数据处理,极大地提高了代码的可读性、可维护性,并且增强了代码的表达能力。

Stream 并不是存储数据的结构,而是数据的操作管道。它是一个 函数式风格 的 API,支持声明式的操作来处理数据流(如过滤、排序、转换等)。

1. Stream 的基本概念

Stream API中的“Stream(流)”,可以看成是一组数据的 集合,我们可以对它施加一系列的数据操作,这些操作可以是 顺序进行的,也可以是并行执行的。

Stream 代表一个元素的队列,这些元素可以是集合、数组或 I/O 渠道中的数据。Stream 本身并不存储数据,它只是一个流,可以对数据进行一系列操作。

  • 声明式:流操作可以用简洁的表达式进行组合,代码更加清晰、可维护。
  • 中间操作与终端操作:Stream API 通过中间操作(如 filtermapflatMap)来处理数据,最后通过终端操作(如 collectreduceforEach)生成结果。
  • 惰性求值:Stream 操作通常是惰性求值的,只有在终端操作执行时才会实际计算。
  • 并行计算支持:Stream 支持通过 parallel() 轻松实现数据的并行计算。
  • 无状态与有状态操作:流操作根据是否需要跨元素之间共享状态分为无状态操作和有状态操作。
  • 管道式操作:Stream 中的操作是通过管道流式连接的,这意味着每个操作都基于前一个操作进行处理,形成一个链式调用。
    请添加图片描述

与集合的区别

但是 Stream虽然看上去是集合,但实际上不是集合。

  • 普通的集合,关注的是数据的存储方式,而Stream,关注的是施加于这些集合元素的处理任务(称为“Operation(操作)”)。
  • 集合讲的是数据,流讲的是计算。
  • Stream通常从普通的集合中“抽取”数据,但也可以从其它数据源(比如文件和数据库)中提取数据。
  • Stream只“抽取”数据,它从不会修改底层的数据源。简言之:** Stream Operation是只读操作!
  • 只要底层数据源允许,流中包容的元素数目是不受限制的。
  • Stream API采用一种 “即抽取、即使用、即丢弃” 的方式处理数据,不需要“ 把所有数据都加载到内存中(而普通集合就是这 样的)”才能工作,所以,能处理很大的数据集。

和Java I/O 的Stream的区别

在 Java 中,Stream APII/O 中的 Stream 都使用了相同的名字,但它们代表了两个完全不同的概念。尽管它们都涉及数据的流动(流式处理),但它们的用途、实现方式、处理数据的方式和目标都大不相同。

Java I/O 中的 Stream 主要用于数据的输入和输出,是 Java 的传统 I/O 机制的一部分。它用于从不同的数据源(如文件、网络连接、内存缓冲区等)读取数据,或将数据写入目标位置。I/O 中的 Stream 是面向字节或字符的流,是面向操作系统层面的 I/O 处理。(后续会单独发关于I/O流的文章)ヾ(๑╹◡╹)ノ"
Stream API 用于处理数据集合(如 List, Set, Map)的元素,它与传统的 I/O 流无关。Stream API 使得在集合中以声明式方式进行复杂的数据处理变得非常简单、直观,并支持并行计算。Stream 主要通过 函数式编程 的方式处理集合数据,而不是像 I/O 流那样依赖操作系统的底层实现。

直白点说,Stream API中的Stream,可以看成一种“特殊的集合”,我们可以向这个集合施加一系列的“操作(Operation)”, 而Java IO中的Stream,实际上是一个“数据流”,它是由一连串有序的数据(字节或字符)所构成的一个“数据序列”,是被加工的“原材料”

特性Java I/O StreamJava Stream API
目的用于读写数据,进行 I/O 操作用于对集合数据进行处理(如过滤、映射、排序等)
操作方式顺序处理数据,一个字节/字符一个字节/字符地读取或写入操作数据集中的元素,可以链式调用,声明式处理
操作模式阻塞式 I/O函数式编程,支持惰性求值
操作对象物理文件、网络、内存等 I/O 数据流集合、数组、文件等内存中的数据
并发支持无并发支持支持并行流操作(通过 parallel()
状态每次读取会改变流的位置流操作是无状态或有状态的,惰性执行
返回值通常操作数据本身(读/写)返回一个新的 Stream 或最终结果

2. 创建 Stream

Stream 可以从不同的数据源创建,最常见的是通过集合、数组、文件或 I/O 流创建。

构建一个空流

在有些情况下,需要构建一个“空”的流,调用Stream.empty() 方法可以达到这个目的:

//构建一个空的字符串流  
Stream<String> stream = Stream.empty();  

//构建一个空的整数流  
IntStream numbers = IntStream.empty();
通过集合创建 Stream
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

List<String> list1 = Arrays.asList("a", "b", "c", "d");

// 创建 Stream
Stream<String> stream1 = list.stream();
通过数组创建 Stream

JDK中的Arrays类提供了直接基于现有数组创建流的静态方法stream()
Arrays.stream()方法提供了用于处理int[]、double[] 与long[]型数组的重载形式,生成相应的原始数值流。

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


//注意一下流的类型比较特殊,是IntStream
int[] numbers = {2, 3, 5, 7, 11, 13};  
var arrayStream = Arrays.stream(numbers);

通过静态方法创建 Stream

Stream接口定义了一个of方法,可以用于创建流
在Java标准库中,of方法的实现实际上被委托给 java.util.Arrays类定义的stream方法,基于 数组创建流。

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

IntStream stream = IntStream.of(1, 1, 2, 3, 5);
通过生成器方法创建 Stream
//有些流的元素需要依据它的前一个元素才能确定,对于这种情况,可以使用Stream.iterate()方法构建流对象:
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 无限流

//生成的流包容 [0,100) 区间的所有整数  
IntStream zeroToNinetyNine = IntStream.range(0, 100);  

//生成的流包容 [0,100]区间的所有整数  
IntStream zeroToHundred = IntStream.rangeClosed(0, 100);

使用StreamBuilder创建流

使用Stream.Builder<T>也能创建流

Stream<String> stream = Stream.<String>builder()  
        .add("Ken")  
        .add("Jeff")  
        .add("Chris")  
        .add("Ellen")  
        .build();  

数值流与装箱流

Java 8 引入了三个原始类型流接口:IntStream、 DoubleStream 和LongStream,其流中的元素为int、long 和double这样的原始数据类型,在处理数据时,可以避免装 箱带来的性能损失。
请添加图片描述

应用实例:随机数流

使用Stream.generate()方法,配合JDK中所提供的Math.random() 方法,可以很方便地生成特定区间内的随机整数。

//使用generate()方法生成[from,to]之间的随机整数  
private static void useGenerate(int from, int to) {  
    Stream.generate(Math::random)  
            .limit(5)  
            .map(doubleNum -> (int) (doubleNum * (to - from + 1)) + 1)  
            .forEach(System.out::println);  
}

由于程序中经常需要生成随机的整数和浮点数,JDK8中为Random类添加 了ints()/longs()/doubles()方法创建相应的数值流。

//基于JDK所提供的Random类方法生成随机数流  
private static void useRandomClass() {  
    //生成[0,100)区间中的随机整数  
    new Random()  
            .ints(0, 100)  
            .limit(5)  
            .forEach(System.out::println);  
  
    //生成[100,200)区间内的随机浮点数  
    new Random()  
            .doubles(100, 200)  
            .limit(5)  
            .forEach(System.out::println);  
}
装箱流

有些常用的流操作,无法用于数值流,这时,可以使用 boxed方法或mapToObj方法 将数值流转换对象流,就可以使用这些操作了。

//以下代码无法编译  
//IntStream.of(3, 1, 4, 1, 5, 9)  
//        .collect(Collectors.toList());  
  
//解决之道:将int用boxed方法转换为Integer,就可以编译了  
List<Integer> ints = IntStream.of(3, 1, 4, 1, 5, 9)  
        .boxed()  
        .collect(Collectors.toList());  
//输出:[3, 1, 4, 1, 5, 9]  
System.out.println(ints);  
  
//另一种方式:使用mapToObject,将其转换为对象  
List<Integer> ints2 = IntStream.of(3, 1, 4, 1, 5, 9)  
        .mapToObj(Integer::valueOf)  
        .collect(Collectors.toList());  
//输出:[3, 1, 4, 1, 5, 9]  
System.out.println(ints2);

文件、文件夹流

我们可以基于一个文本文件中的所有行构建流,也可以基于一个文件夹中的所有子文件夹构建流。

java.nio.file.Files 中的很多静态方法都会返回一个流。

下面这个例子就是读取data.txt的内容作为字符串流

private static void useNIOFileStream() {  
    //lines()方法,返回的就是一个字符串流  
    try (Stream<String> lines = Files.lines(  
            Paths.get("data.txt"))) {  
        lines.forEach(System.out::println);  
    } catch (IOException ex) {  
        System.out.println(ex.getMessage());  
    }  
}

这个例子则是

// 列出当前文件夹下的所有文件  
public static void listFileTree() {  
    Path dir = Paths.get("");  
    System.out.printf(" %s contains:\n", dir.toAbsolutePath());  
    //walk方法返回一个Stream  
    try (Stream<Path> fileTree = Files.walk(dir)) {  
        fileTree.forEach(System.out::println);  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
}

3. Stream 操作

Stream API 操作分为两种类型:

  1. 中间操作(Intermediate Operations):这些操作会返回一个新的 Stream,它们是惰性求值的,只有在终端操作触发时才会执行。
  2. 终端操作(Terminal Operations):这些操作会消耗 Stream,产生一个结果(如:集合、单个值、布尔值等),一旦终端操作执行,Stream 就会被消耗掉,无法再次使用。

对集合对象调用stream()方法,获取Stream对象的引用,之后,可以级联调用多个Stream API函数,最后, 需要调用一个“终结操作”。
![[Pasted image 20241129123453.png]]

常见的中间操作

流的筛选元素
  • filter(Predicate<T> predicate): 通过一个过滤条件来筛选流中的元素。
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    names.stream()
         .filter(name -> name.startsWith("A"))  //选择首字母为'A'的字符串通过
         .forEach(System.out::println); // 输出:Alice
    

    括号内的name -> name.startsWith("A"),是一个简化的Lambda 表达式,详细内容可以参考这个文章.lambda 表达式
    请添加图片描述

takeWhile: 遇到第一个不满足条件的元素时停止
  • 作用:从流的开始位置依次提取元素,直到遇到第一个不满足条件的元素为止。
    • 一旦条件不满足,就停止提取,后续的元素不再检查。
    • 提取的元素形成一个新的流。
  • 用法场景
    • 适用于已排序流或对流中元素顺序敏感的场景。
    • 用来从流的起始部分提取满足某条件的连续元素。
//提取小于 5 的元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 6, 4, 5, 7);
// 提取前几个小于 5 的连续元素
List<Integer> result = numbers.stream()
                              .takeWhile(n -> n < 5)
                              .toList();
System.out.println(result); // 输出:[1, 2, 3]
  • 过程
    1. 从第一个元素开始检查条件 n < 5
    2. 当遇到元素 6 不满足条件时,停止提取。
    3. 输出结果是 [1, 2, 3],因为只有这些元素在 6 之前满足条件。
dropWhile: 遇到第一个不满足条件的元素时停止
  • 作用:从流的开始位置开始丢弃元素,直到遇到第一个不满足条件的元素为止。
    • 一旦条件不满足,就保留剩下的所有元素。
    • 丢弃的元素不会出现在新流中。
  • 用法场景
    • 适用于已排序流或对流中元素顺序敏感的场景。
    • 用来跳过满足某条件的初始连续元素,保留剩下的部分。
//丢弃小于 5 的元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 6, 4, 5, 7);
// 丢弃前几个小于 5 的连续元素
List<Integer> result = numbers.stream()
                              .dropWhile(n -> n < 5)
                              .toList();
System.out.println(result); // 输出:[6, 4, 5, 7]
  • 过程
    1. 从第一个元素开始检查条件 n < 5
    2. 当遇到元素 6 不满足条件时,停止丢弃。
    3. 输出结果是 [6, 4, 5, 7],因为 6 之后的所有元素被保留。
方法作用处理方式结果流
takeWhile提取从开始满足条件的所有连续元素,遇到第一个不满足条件的元素时停止提取只要不满足条件,就停止提取剩余元素包含满足条件的连续前缀元素
dropWhile丢弃从开始满足条件的所有连续元素,遇到第一个不满足条件的元素时停止丢弃只要不满足条件,就保留剩余元素包含不满足条件的第一个元素及其后续部分

** filtertakeWhiledropWhile 的区别**
  • filter
    • 对流中的所有元素逐一进行条件检查,保留所有满足条件的元素。
    • 检查整个流,即使流是无序的。
    • 不会因为第一个不满足条件的元素而停止。

示例:filter 保留小于 5 的所有元素

List<Integer> numbers = Arrays.asList(1, 2, 3, 6, 4, 5, 7);
// 保留所有小于 5 的元素
List<Integer> result = numbers.stream()
                              .filter(n -> n < 5)
                              .toList();
System.out.println(result); // 输出:[1, 2, 3, 4]
  • 区别
    • takeWhiledropWhile 是惰性截断操作,适用于有序流。
    • filter 遍历整个流,适用于无序流。
  1. takeWhiledropWhile 的局限性

    • 对于无序流,这两个方法的结果可能没有意义。

    • 示例:

      Set<Integer> numbers = new HashSet<>(Arrays.asList(3, 1, 2, 6, 4, 5));
      var result = numbers.stream().takeWhile(n -> n < 5).toList();
      System.out.println(result); // 输出不确定,依赖流的实际顺序
      
  2. 短路操作

    • 它们会在条件失败时停止对流的检查,因此对于大型数据集性能较优。
  3. filter 搭配使用

    • 可以用 takeWhiledropWhile 快速分割流,然后用 filter 对不同部分分别处理。
方法主要功能停止操作的条件推荐使用场景
takeWhile提取满足条件的前缀元素第一个不满足条件的元素从流中提取前缀部分,如连续满足条件的记录
dropWhile丢弃满足条件的前缀元素第一个不满足条件的元素跳过流的前缀部分,保留后续部分
filter筛选所有满足条件的元素流的全部元素都需要检查过滤整个流,与流是否有序无关
流的转换map

使用map()方法可以把一种类型的数据经过特定的加工与处理之后,转换为另 一种类型的数据。
请添加图片描述
在实际开发中,经常需要将数据库中的数据记录,抽取部分字段值,传给客 户端,这种封装了发给客户端信息的对象,通常称为“DTO(数据传输对 象)”。
使用 map 方法可以轻松实现 DTO的转换,常用于将实体类转换为轻量级的 DTO 类。

  • 目标:将复杂的实体类对象转换为轻量级的对象(通常只包含需要传输或展示的字段)。
  • 场景
    • 将数据库中的实体类(Entity)转换为 REST API 的返回对象(DTO)。
    • 简化数据传输,隐藏不必要或敏感的信息。
  • 步骤
    1. 获取实体类集合。
    2. 调用 stream() 方法创建流。
    3. 使用 map 方法将实体类转换为 DTO 对象。
    4. 使用 collect(Collectors.toList()) 将流结果收集为目标集合。

实体类与 DTO 类

class User {
    private int id;
    private String name;
    private String email;
    private String password; // 不需要在 DTO 中暴露

    // 构造函数
    public User(int id, String name, String email, String password) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.password = password;
    }

    // Getter
    public int getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

class UserDTO {
    private int id;
    private String name;
    private String email;

    // 构造函数
    public UserDTO(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    @Override
    public String toString() {
        return "UserDTO{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + '}';
    }
}

DTO 转换逻辑:

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

public class DtoConversionExample {
    public static void main(String[] args) {
        // 创建用户实体集合
        List<User> users = Arrays.asList(
                new User(1, "Alice", "alice@example.com", "password123"),
                new User(2, "Bob", "bob@example.com", "securePassword"),
                new User(3, "Charlie", "charlie@example.com", "admin123")
        );

        // 使用 map 方法将 User 转换为 UserDTO
        List<UserDTO> userDTOs = users.stream()
                .map(user -> new UserDTO(user.getId(), user.getName(), user.getEmail()))
                .collect(Collectors.toList());

        // 输出结果
        userDTOs.forEach(System.out::println);
    }
}

** 执行结果**:

UserDTO{id=1, name='Alice', email='alice@example.com'}
UserDTO{id=2, name='Bob', email='bob@example.com'}
UserDTO{id=3, name='Charlie', email='charlie@example.com'}

优化与扩展

使用方法引用优化 map

如果转换逻辑可以抽取到一个独立方法中,可以用方法引用优化代码。

// 提取转换逻辑
private static UserDTO convertToDTO(User user) {
    return new UserDTO(user.getId(), user.getName(), user.getEmail());
}

// 使用方法引用
List<UserDTO> userDTOs = users.stream()
        .map(DtoConversionExample::convertToDTO)
        .collect(Collectors.toList());

处理复杂场景

如果 DTO 中包含多个层级的属性(嵌套对象),可以在 map 中对每一层分别处理。
例如,UserDTO 包含嵌套的 AddressDTO

class AddressDTO {
    private String city;
    private String zipCode;

    public AddressDTO(String city, String zipCode) {
        this.city = city;
        this.zipCode = zipCode;
    }
}

转换逻辑:

.map(user -> new UserDTO(
        user.getId(),
        user.getName(),
        user.getEmail(),
        new AddressDTO(user.getAddress().getCity(), user.getAddress().getZipCode())
))
  • map(Function<T, R> mapper): 将每个元素映射成另外一个对象。
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    names.stream()
         .map(String::toUpperCase)
         .forEach(System.out::println); // 输出:ALICE, BOB, CHARLIE, DAVID
    
  • flatMap(Function<T, Stream<R>> mapper): 将每个元素转换成一个流,然后合并所有流为一个流。

如果生成的结果是“元素是流的流(stream of stream)”,则需要使用 flatMap()将形成嵌套关系的两个流合成为一个。

```java
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b"), Arrays.asList("c", "d"), Arrays.asList("e", "f"));
listOfLists.stream()
           .flatMap(List::stream)
           .forEach(System.out::println); // 输出:a, b, c, d, e, f
```
  • 流的查找单个元素

这两个方法都用于从流中查找单个元素,但它们之间有一些关键的区别,尤其是在并行流(parallel streams)中使用时。

  • findAny() 是用于返回流中的任意元素,在并行流中有更好的性能,因为它不关心元素的顺序。
  • findFirst() 是确保返回流中的第一个符合条件的元素,不管是顺序流还是并行流,它都会返回流中的第一个匹配元素,可能在并行流中性能稍差。
1. findAny() 方法
  • 作用:返回流中的任意一个元素。

  • 返回值:返回 Optional<T> 类型(因为可能会返回null,后面会细讲这个类的)的结果,表示流中找到的一个元素(如果存在)。

  • 特性findAny() 查找的是流中的任意一个元素,在顺序流和并行流中行为可能不同。对于顺序流来说,它通常返回流中遇到的第一个元素,但在并行流中,findAny() 可以返回任意一个符合条件的元素,这通常是性能优化的结果。

    在并行流中findAny() 会尽量选择一个可能更容易找到的元素,因此它不保证顺序和特定元素。多线程的并行执行使得findAny()的返回结果在不同的执行中可能是不同的。

    示例

    static Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    var anyResult = Arrays.stream(numbers)
                          .filter(n -> n < 10)
                          .findAny();
    anyResult.ifPresent(System.out::println); // 输出:1(可能是任意一个符合条件的元素)
    
    • 在顺序流中,findAny() 通常返回第一个符合条件的元素(例如 1)。
    • 在并行流中,findAny() 可能返回任何一个符合条件的元素(比如并行处理时可能返回 53 等,顺序不确定)。
2. findFirst() 方法
  • 作用:返回流中的第一个元素。

  • 返回值:返回 Optional<T> 类型的结果,表示流中的第一个元素(如果存在)。

  • 特性findFirst() 会保证返回流中的第一个符合条件的元素。如果流是顺序流,那么它始终会返回第一个符合条件的元素。

    • 对于顺序流,它会依次处理流中的每个元素,返回第一个符合条件的元素。
    • 对于并行流,尽管流中的元素可以并行处理,但findFirst()仍然保持顺序,确保返回流中的第一个符合条件的元素。这意味着 findFirst() 在并行流中的性能可能较差,因为它需要等待流中的所有线程完成操作,以确保能够正确地找到第一个元素。

    示例

    static Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    var firstResult = Arrays.stream(numbers)
                            .filter(n -> n < 10)
                            .findFirst();
    firstResult.ifPresent(System.out::println); // 输出:1(始终返回第一个符合条件的元素)
    
    • 在顺序流中,findFirst() 始终会返回第一个符合条件的元素。
    • 在并行流中,findFirst() 仍然保证返回第一个符合条件的元素,但可能会因为等待并行任务而影响性能。
特性findAny()findFirst()
适用场景任意查找元素,通常用于并行流场景中的性能优化查找第一个符合条件的元素,适用于顺序流
顺序性不保证顺序,尤其在并行流中保证顺序,始终返回第一个符合条件的元素
并行流并行流中可能会返回流中的任意一个符合条件的元素并行流中也会返回第一个符合条件的元素,性能差
性能在并行流中表现较好(因为可以不关心顺序)性能较差(因为需要确保顺序)
其他
  • distinct(): 去除重复元素。
    List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4);
    numbers.stream()
           .distinct()
           .forEach(System.out::println); // 输出:1, 2, 3, 4
    
  • sorted(): 排序流中的元素。

    默认是升序排序

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

要自定义的话,需要提供一个比较器对象。

List<String> words = List.of("a","bcd","ef","ghij");
Stream<String> sortedWords = 
		words.stream().sorted(Comparator.comparing(String::length));
		sortedWords.forEach(System.out::println)
peek(Consumer<? super T> action): 调试或打印流中的每个元素(不会改变流)。

peek的功能与forEach()非常类似,不同之处在于peek是一个中间操 作,可以级联其他Stream API操作,而forEach则是一个终结操作。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    names.stream()
         .peek(name -> System.out.println("Processing: " + name))
         .map(String::toUpperCase)
         .forEach(System.out::println);
  • peek 的作用:在 map 操作之前,每个元素会被传递给 peek,执行System.out.println("Processing: " + name)
  • 输出了类似调试信息,如 Processing: Alice
  • 它不会改变流中的数据,而是提供一种“旁观”或“查看”流中数据的方式。
  • 这里的 peek 可以帮助观察流在被操作时的状态和流经的元素。
limit() 和 skip():限制元素通过个数 / 跳过元素个数

Stream<T> limit(long maxSize);

var nums= List.of(1,2,3,4,5,6);  
//取出前3个元素输出  
nums.stream().limit(3).forEach(System.out::println);

Stream<T> skip(long n);

var nums= List.of(1,2,3,4,5,6);  
//忽略前3个元素,输出后面的元素  
nums.stream().skip(3)  
        .forEach(System.out::println);

常见的终端操作

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

就像前面出现多次的forEach(System.out::println)语句,执行后就是输出流中的每个元素。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    names.stream()
         .forEach(System.out::println); // 输出:Alice, Bob, Charlie, David
  • reduce(BinaryOperator<T> accumulator): 对流中的元素进行归约操作,将多个元素合并成一个结果。比如计算总和、乘积等。

    ![[Pasted image 20241129220946.png]]

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    int sum = numbers.stream()
                     .reduce(0, Integer::sum); // 求和,当然括号里也能换成“0, (a, b) -> a + b”
    System.out.println(sum); // 输出:10
    
  • anyMatch(Predicate<T> predicate): 判断流中是否有任何一个元素匹配给定的条件。
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    boolean result = names.stream()
                          .anyMatch(name -> name.startsWith("A"));
    System.out.println(result); // 输出:true
    
  • allMatch(Predicate<T> predicate): 判断流中是否所有元素都满足给定的条件。
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    boolean result = names.stream()
                          .allMatch(name -> name.length() > 3);
    System.out.println(result); // 输出:true
    
  • noneMatch(Predicate<T> predicate): 判断流中是否没有任何元素匹配给定的条件。
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    boolean result = names.stream()
                          .noneMatch(name -> name.startsWith("Z"));
    System.out.println(result); // 输出:true
    
collect收集器

collect顺次收集流中的所有元素,进行某种处理之后得到一个结果, 它是一个终端操作。
完成“收集”操作的对象,称为“收集器Collector”。

  • 用途
    • 合并流中的元素。
    • 将流中的数据转换为另一种形式。
    • 聚合流中的数据(如求和、分组)。
Collectors

Collectors 是一个工具类,位于 java.util.stream 包中,提供了一些常用的 Collector 工具方法。通过这些方法,我们可以直接使用预定义的收集器进行操作,而不需要手动实现。

  • 常见功能
    • 转换为集合toList()toSet()
    • 字符串连接joining()
    • 数据统计counting()averagingInt()
    • 数据分组groupingBy()
    • 数据分区partitioningBy()
常用 Collector 示例**

1)将流转换为集合

  • 使用 toList()toSet() 收集器将流中的元素转换为集合。

代码示例

static void distinctWithSet() {
    var nums = List.of(1, 1, 3, 5, 9, 3, 8);
    // 去重并转换为 Set
    Set<Integer> distinctNumbers = nums.stream()
            .collect(Collectors.toSet());
    System.out.println(distinctNumbers);
    // 输出:[1, 3, 5, 9, 8]
}

2)将流转换为字符串

  • 使用 joining() 将流中的元素拼接成一个字符串。

代码示例

static void useStringCollectors() {
    var numbers = List.of(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1);
    // 将流中的数字转换为字符串并用逗号连接
    var numString = numbers.stream()
            .map(String::valueOf)
            .collect(Collectors.joining(","));
    System.out.println(numString);
    // 输出:1,2,3,4,5,6,5,4,3,2,1
}

3)映射与收集

  • 使用 mapping() 对流中的元素进行转换,并将结果收集到集合中。
    代码示例
static void useMappingCollector() {
    var students = Student.getExampleStudents();
    // 将 Student 对象的名称提取到 List 中
    Function<Student, String> getStudentName = Student::getName;
    List<String> nameList = students.stream()
            .collect(Collectors.mapping(getStudentName, Collectors.toList()));
    System.out.println(nameList);
    // 输出:[张三, 李四, 王五]
}

4)统计最小值

  • 使用 minBy() 找到流中的最小元素。

代码示例

static void useMinByCollector() {
    var students = Student.getExampleStudents();
    // 使用 GPA 比较器找到最小值
    var gpaComparator = Comparator.comparing(Student::getGpa);
    var minResult = students.stream()
            .collect(Collectors.minBy(gpaComparator));
    minResult.ifPresent(System.out::println);
}

5)分组收集

  • 使用 groupingBy() 按条件对流中的元素进行分组。

代码示例

static void useGroupCollectors() {
    var numbers = List.of(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1);

    // 按奇偶分组
    var groupResult = numbers.stream()
            .collect(Collectors.groupingBy(num -> num % 2 == 0 ? "even" : "odd"));
    System.out.println(groupResult);
    // 输出:{even=[2, 4, 6, 4, 2], odd=[1, 3, 5, 5, 3, 1]}

    // 分组并过滤大于等于 50 的数字
    var evenAndOddNumbersGreaterThan50 = IntStream.rangeClosed(1, 100)
            .boxed()
            .collect(Collectors.groupingBy(
                    num -> num % 2 == 0 ? "even" : "odd",
                    Collectors.filtering(num -> num >= 50, Collectors.toList())));
    System.out.println(evenAndOddNumbersGreaterThan50);
    // 输出:{even=[50, 52, ..., 100], odd=[51, 53, ..., 99]}
}

6)分区收集

  • 使用 partitioningBy() 根据条件将流中的元素分为两组。

代码示例

static void usePartitioningCollectors() {
    // 按分数是否及格分区
    var doYouPassed = IntStream.rangeClosed(1, 100)
            .boxed()
            .collect(Collectors.partitioningBy(score -> score >= 60));
    System.out.println(doYouPassed);
    // 输出:{false=[1, ..., 59], true=[60, ..., 100]}

    // 分区后进一步分组
    var doYouPassed2 = IntStream.rangeClosed(1, 100)
            .boxed()
            .collect(Collectors.partitioningBy(
                    score -> score >= 60,
                    Collectors.groupingBy(score -> {
                        int temp = score / 10;
                        return switch (temp) {
                            case 0, 1, 2, 3, 4, 5 -> "不及格";
                            case 6 -> "及格";
                            case 7 -> "中";
                            case 8 -> "良";
                            case 9, 10 -> "优";
                            default -> "无效成绩";
                        };
                    })));
    System.out.println(doYouPassed2);
}

Optional类型

由于Stream API中经常需要“级联”多个操作,因此,如果中间某个操作遇到null引用时,如何处理它比较麻烦。
默认情况下,如果针对null引用调用某个类的实例方法,JVM会抛出 NullPointerException ,如果不做妥善处理,这个异常可能会“打断”流处理管线。为了解决这个问题,所以需要Optional<T>类。
通过显式地表示值是否存在,减少 NullPointerException(NPE)的风险。

请添加图片描述

Optional对象是一个“可能为null” 的对象。
所有那些有可能返回一个null引用的操作,都应该返回一个Optional对象。
Optional对象定义了一个 isPresent()方法用于确定它是否包容一个有效的值。如果值有效,可以 调用它的get()方法提取出这个值。
如果isPresent()返回false,代码又尝试调用它的get()方法,JVM会抛 出一个NoSuchElementException 。

** 创建 Optional 对象**
空的 Optional 对象
  • 用于表示值不存在。
Optional<String> empty = Optional.empty();
包含非空值的 Optional
  • 用于包装一个非空值。
Optional<String> value = Optional.of("Hello, World!");
允许空值的 Optional
  • 用于包装一个可能为 null 的值。
  • obj==null时,此方法返回Optional.empty(),否则, 返回Optional.Of(obj)
String obj = null;
Optional<String> optional = Optional.ofNullable(obj);

访问 Optional 的值
检查是否存在值
Optional<String> optional = Optional.of("Hello");

if (optional.isPresent()) {
    System.out.println("Value is present: " + optional.get());
}
处理值存在或不存在的情况
  • 使用 ifPresent 执行操作:
optional.ifPresent(value -> System.out.println("Value: " + value));
  • 使用 orElse 提供默认值:
String result = optional.orElse("Default Value");
System.out.println(result);
  • 使用 orElseGet 提供默认值的工厂:
String result = optional.orElseGet(() -> "Generated Default Value");
System.out.println(result);
  • 使用 orElseThrow 抛出异常:
String result = optional.orElseThrow(() -> new IllegalArgumentException("No value present"));

** 常用方法**
1)map
  • 用于转换 Optional 的值。
    Optional类中定义有一个map方法,可以对结果进行类型转换, 得到另一个Optional对象:
Optional<String> optional = Optional.of("Hello");
Optional<Integer> length = optional.map(String::length);
length.ifPresent(System.out::println); // 输出:5
2)flatMap
  • 类似 map,但需要返回另一个 Optional
Optional<String> optional = Optional.of("Hello");
Optional<String> upper = optional.flatMap(val -> Optional.of(val.toUpperCase()));
upper.ifPresent(System.out::println); // 输出:HELLO
3)filter
  • 用于过滤值。
Optional<String> optional = Optional.of("Hello");
optional.filter(val -> val.startsWith("H"))
        .ifPresent(System.out::println); // 输出:Hello

使用场景
1)避免 null 检查

代替传统的 null 检查:

// 传统方式
if (obj != null) {
    // 处理
}

// Optional 方式
Optional.ofNullable(obj).ifPresent(value -> {
    // 处理
});
2)处理返回值
  • 方法返回值可能为空时,可以返回 Optional 类型,强制调用方检查值是否存在。
public Optional<User> findUserById(int id) {
    return Optional.ofNullable(userRepository.findById(id));
}
3)链式操作
  • 结合 mapflatMap,实现更简洁的链式调用。
Optional<String> result = Optional.of("  Hello  ")
                                  .map(String::trim)
                                  .map(String::toUpperCase);
result.ifPresent(System.out::println); // 输出:HELLO

** Optional 的优缺点**
优点
  • 明确表示值是否存在,减少 null 检查。
  • 提供丰富的工具方法,支持流式操作。
  • 代码更清晰,逻辑更明确。
缺点
  • 对于性能敏感的场景,可能会引入额外的开销。
  • 不适合用于集合或大批量数据。

最后举个栗子:

import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        // 创建 Optional 对象
        Optional<String> optional = Optional.ofNullable("Hello");

        // 使用 map 转换值
        optional.map(String::toUpperCase)
                .ifPresent(System.out::println); // 输出:HELLO

        // 使用 orElse 提供默认值
        String result = optional.orElse("Default");
        System.out.println(result); // 输出:HELLO

        // 使用 filter 过滤值
        optional.filter(value -> value.startsWith("H"))
                .ifPresent(System.out::println); // 输出:HELLO
    }
}

4. Stream 操作的特点

  • 惰性求值(Lazy Evaluation):只有在执行终端操作时,流中的中间操作才会被触发。中间操作会返回新的流,可以进行链式调用。
  • 管道化(Pipelining):Stream 操作被设计成管道式的,多个操作可以串联在一起。这有助于实现流式操作和简洁的代码。
  • 短路操作(Short-circuiting):某些操作会提前终止流的处理(如 anyMatchfindFirst 等),从而提高性能。例如,假设需要对一个 用and运算符连起来的包容多个布尔子表达式的大的布尔表达式求值。 不管表达式有多长,只需发现其中有一个子表达式为false,就可以 推断整个大的表达式将返回 false,用不着把所有子表达式全部都 计算一遍,这就是“短路”。
    对于流而言,某些操作(例如 findFirst 和 findAny)不用处理整个流, 只要找到一个元素,就可以有结果了,它们就是“短路操作”。
    同样,limit也是一个短路操作——它只需要创建一个给定大小的流,也用不 着处理流中所有的元素。对于无限流,limit操作把无限流变成有限流。

5. 并行流(Parallel Stream)

Stream 支持并行操作。通过调用 parallel(),你可以将流操作转为并行处理,从而利用多核 CPU 提升性能。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
                 .reduce(0, Integer::sum); // 并行求和
System.out.println(sum); // 输出:15

注意:

  • 并行流适合数据量较大的操作,且流操作是无状态的。
  • 使用并行流时,可能会增加线程的上下文切换开销,因此需要谨慎使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梓仁沐白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值