前言

Java Stream是Java 8引入的一个新的API,它提供了一种更简洁、更灵活的处理集合数据的方式。通过Stream,我们可以利用函数式编程的思想来处理集合数据,例如筛选、映射、归约等操作,让代码更加简洁、可读性更高。在开始学习Java 8的Stream API之前,了解匿名内部类(Anonymous Inner Classes)和Lambda表达式确实是很重要的,因为它们与Stream API中的操作密切相关。匿名内部类是一种没有名称的内部类,通常用于实现单个方法或需要一次性使用的接口。Lambda表达式则是一种更简洁的方式来实现匿名内部类,特别是在与函数式接口(Functional Interface)一起使用时。

匿名内部类

匿名内部类在Java中用于创建不需要单独命名的类,它们通常用于实现接口或继承类。例如,如果你需要创建一个实现了Runnable接口的线程,而这个线程只需要执行一次,使用匿名内部类可以简化代码:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}).start();

Lambda表达式

Java 8引入了Lambda表达式,它允许你以更简洁的方式实现函数式接口。Lambda表达式可以看作是匿名内部类的简化版本。在上面的例子中,使用Lambda表达式可以写成:

new Thread(() -> System.out.println("Thread is running")).start();

Lambda表达式由参数列表、箭头(->)和代码块组成。如果函数式接口的方法只有一个参数,参数列表的圆括号可以省略,如果方法体只有一条语句,花括号也可以省略。

函数式接口

函数式接口是只包含一个抽象方法的接口。Java 8中,许多新的接口如RunnableCallableComparator等都是函数式接口。这些接口可以通过Lambda表达式或匿名内部类来实现。

举例

假设我们有一个包含一组数字的List,我们想要对其中的偶数进行平方处理,然后求和。使用传统的方式,我们需要使用循环来遍历List,然后进行判断和计算。而使用Java Stream,我们可以通过一行代码来实现这个功能:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); 
int sum = numbers.stream() //将List转换为Stream
.filter(n -> n % 2 == 0) //使用filter方法筛选出偶数
.map(n -> n * n) //使用map方法将偶数进行平方处理
.reduce(0, Integer::sum); //使用reduce方法求和
System.out.println(sum); // 输出 56

通过上面的例子,我们可以看到Java Stream的强大之处:简洁、灵活、函数式。掌握Java Stream可以让我们更加高效地处理集合数据,提高代码的可读性和可维护性。

image-rtow.png

流(stream)的类型

Java 8提供了两种创建流的方式:

  1. stream

    • stream 是串行的,意味着它的操作是顺序执行的。

  2. parallelStream

    • parallelStream 是并行的。它的执行不是按顺序的,而是采用了分治策略(如Fork/Join框架)来提高执行效率,充分利用CPU性能。但并行执行存在不确定性,并且不是线程安全的。在某些情况下,使用并行流可能会导致性能下降或产生其他问题。因此,在考虑使用并行流时,需要谨慎评估系统的CPU性能和线程安全性。

map

map 是流(stream)处理中非常核心且常用的一个方法。它用于对流中的每个元素执行特定的操作,并返回一个新的流,其中包含由这些操作产生的结果。这个方法允许你提取对象的某个属性、转换数据类型或执行其他任何映射操作。以下是几个使用 map 方法的示例:

  1. 提取对象中的某个属性:

List<Person> people = ...; // 假设有一个人员列表
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());

在这个例子中,我们从一个包含 Person 对象的列表中提取了每个人的名字,并将它们收集到一个新的字符串列表中。

  1. 转换数据类型:

List<Integer> numbers = ...; // 假设有一个整数列表
List<String> strings = numbers.stream().map(Object::toString).collect(Collectors.toList());

在这个例子中,我们将整数列表转换为字符串列表。每个整数都被转换成其字符串表示形式。

  1. 进行复杂的映射操作:

List<Integer> numbers = ...; // 假设有一个整数列表
List<Double> squaredNumbers = numbers.stream().map(n -> n * n).collect(Collectors.toList());

在这个例子中,我们对列表中的每个数字进行了平方操作,并将结果收集到一个新的列表中。这里使用了 lambda 表达式来定义映射操作。

这些方法展示了 map 方法在流处理中的灵活性和实用性。通过使用 map 方法,你可以对流中的元素进行各种复杂的操作,从而得到你需要的最终结果。

flatMap

在Java 8中,flatMap方法是一个非常实用的工具,它允许我们在Stream API中对流的元素进行函数转换,并将结果合并成一个单一的流。

使用flatMap方法时,我们需要提供一个函数作为参数。这个函数会被应用到流的每一个元素上,并返回一个流对象。这些由函数生成的流,通过flatMap方法会被进一步展平(flatten),最终形成一个单一的、连续的流。通过这种方式,我们可以对流进行复杂的转换和操作,从而实现各种数据处理任务。

 List<String> words = Arrays.asList("你好", "世界");
        List<String> uniqueCharacters = words.stream()
                .map(word -> word.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());
        System.out.println(uniqueCharacters); // 输出: [你, 好, 世, 界]

filter

filter是流处理中非常关键的一个方法,它充当了一个强大的过滤器角色。该方法允许我们根据设定的条件对流中的元素进行筛选,仅保留符合特定条件的元素,从而实现对数据的精细控制和处理。这种方法在数据处理中非常有用,特别是在处理大量数据时,通过设定合适的过滤条件,可以高效地获取我们所需的数据。例如,在一个学生成绩流中,我们可以使用filter方法过滤出所有成绩超过60分的学生信息。

List<Student> passingStudents = students.stream()
                .filter(s -> s.getScore() > 60) //过滤出成绩大于60的数据
                .collect(Collectors.toList());
        passingStudents.forEach(student -> System.out.println("学生姓名:" + student.getName() + ",成绩:" + student.getScore())); // 打印通过的学生信息

拆分出来分析:

  1. 使用 stream() 方法将列表转换为流,以便进行更高级的操作和处理。

List<Student> students = ... // 假设有一个学生列表
Stream<Student> passingStudents = students.stream(); // 将列表转换为流
  1. 使用 filter 方法筛选出成绩超过60分的学生,过滤条件为 s -> s.getScore() > 60。这样可以确保只有满足条件的元素才会继续参与后续操作。

List<Student> filteredStudents = passingStudents.filter(s -> s.getScore() > 60).collect(Collectors.toList()); // 使用filter方法筛选并收集结果
  1. 使用 collect(Collectors.toList()) 将过滤后的流收集回列表,以便后续处理或输出。这一步确保了筛选结果得以保存。

  1. 最后,使用 forEach 方法打印出所有通过的学生信息。这样可以直观地展示筛选结果。

//经过filter方法筛选后,结果已经保存在filteredStudents列表中
filteredStudents.forEach(student -> System.out.println("学生姓名:" + student.getName() + ",成绩:" + student.getScore())); // 打印通过的学生信息

image-ufyu.png

forEach

forEach是一种终端操作,意味着一旦调用,流的处理就会立即执行并结束,后续无法再添加其他的流操作。通常,forEach用于对流中的每个元素执行特定的终端动作,例如进行打印输出、修改数据集合内容或更新数据库等操作。

基本用法
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(System.out::println); // 打印每个元素

在这个例子中,我们使用了方法引用 System.out::println 来简化代码,这等同于传递一个 Consumer 实现,该实现打印每个元素。

使用 Lambda 表达式
list.forEach(e -> System.out.println(e)); // 使用 Lambda 表达式打印每个元素

这里,我们使用了一个 Lambda 表达式来实现 Consumer 接口的 accept 方法。

修改集合中的元素
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
list.forEach(e -> e = e.toUpperCase()); // 将每个元素转换为大写

请注意,上面的代码实际上并不会改变 list 中的元素,因为 forEach 操作中的 e 是对集合中每个元素的副本的引用。要修改列表中的元素,你需要使用 for 循环或者 Listset 方法。

与 Stream API 结合使用

forEach 也经常与 Stream API 结合使用,对流中的元素进行操作:

java
List<String> list = Arrays.asList("a", "b", "c");
list.stream().forEach(System.out::println); // 使用 Stream API 打印每个元素

在这个例子中,我们首先将列表转换为流,然后对流中的每个元素执行打印操作。

处理异常

forEach 本身不会抛出异常,即使 Consumer 的实现中抛出了异常。如果需要处理异常,你需要在 Consumer 实现中包含异常处理逻辑:

list.forEach(e -> {
    try {
        // 可能抛出异常的操作
    } catch (Exception ex) {
        // 异常处理逻辑
    }
});

forEach 是一个非常强大的工具,可以用来替代传统的 for 循环,特别是在使用 Stream API 进行集合操作时。它提供了一种更声明式的方式来处理集合中的元素。

PS: forEach 是在开发中使用最多一个APi之一!

distinct

distinct操作对流进行去重,其核心机制是通过对象的哈希码hashcode和相等性equals判断来识别重复元素。当我们需要对自定义对象进行去重时,可以通过重写对象的hashCodeequals方法,确保按照我们期望的逻辑来判断对象是否相同,从而达到理想的去重效果。

List<Person> distinctPeople = people.stream()
                .distinct() // 去重
                .collect(Collectors.toList());
        distinctPeople.forEach(System.out::println);

peek

peek是一种中间操作,它返回一个新的流,并允许在此基础上连续调用其他流操作。此操作主要用于调试目的,可以让你在数据流中间查看每个元素的状态,同时不会干扰到流的正常处理流程。通过peek,你可以更直观地了解数据流在管道中的状态,从而更好地进行调试和优化。

peek 方法主要用于调试。查看例子:

Stream.of(10, 11, 12, 13)
 .filter(n -> n % 2 == 0)
 .peek(e -> System.out.println("Debug filtered value: " + e))
 .map(n -> n * 10)
 .peek(e -> System.out.println("Debug mapped value: " + e))
 .collect(Collectors.toList());

输出:

Debug filtered value: 10
Debug mapped value: 100
Debug filtered value: 12
Debug mapped value: 120

在这个例子中,我们有一个整数流。首先我们将过滤,然后调试,然后映射,然后再次调试。

并行流中的 peek

在并行流操作中,peek 方法可以在上游操作使元素可用的任何时间和线程中被调用。因此 peek 方法接收到的元素可能会因多次运行而有所不同。查看例子:

List<Integer> sortedList = Stream.of(15, 10, 17, 11)
      .parallel()
      .sorted()
      .peek(e -> System.out.println("Debug: " + e))
      .collect(Collectors.toList());
System.out.println("---After sorting---");
System.out.println(sortedList);

输出:

Debug: 15
Debug: 11
Debug: 17
Debug: 10
---After sorting---
[10, 11, 15, 17]