Java Stream 深入浅出 - 01
前言
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中,许多新的接口如Runnable
、Callable
、Comparator
等都是函数式接口。这些接口可以通过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可以让我们更加高效地处理集合数据,提高代码的可读性和可维护性。
流(stream)的类型
Java 8提供了两种创建流的方式:
-
stream:
-
stream
是串行的,意味着它的操作是顺序执行的。
-
-
parallelStream:
-
parallelStream
是并行的。它的执行不是按顺序的,而是采用了分治策略(如Fork/Join框架)来提高执行效率,充分利用CPU性能。但并行执行存在不确定性,并且不是线程安全的。在某些情况下,使用并行流可能会导致性能下降或产生其他问题。因此,在考虑使用并行流时,需要谨慎评估系统的CPU性能和线程安全性。
-
map
map
是流(stream)处理中非常核心且常用的一个方法。它用于对流中的每个元素执行特定的操作,并返回一个新的流,其中包含由这些操作产生的结果。这个方法允许你提取对象的某个属性、转换数据类型或执行其他任何映射操作。以下是几个使用 map
方法的示例:
-
提取对象中的某个属性:
List<Person> people = ...; // 假设有一个人员列表
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());
在这个例子中,我们从一个包含 Person
对象的列表中提取了每个人的名字,并将它们收集到一个新的字符串列表中。
-
转换数据类型:
List<Integer> numbers = ...; // 假设有一个整数列表
List<String> strings = numbers.stream().map(Object::toString).collect(Collectors.toList());
在这个例子中,我们将整数列表转换为字符串列表。每个整数都被转换成其字符串表示形式。
-
进行复杂的映射操作:
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())); // 打印通过的学生信息
拆分出来分析:
-
使用
stream()
方法将列表转换为流,以便进行更高级的操作和处理。
List<Student> students = ... // 假设有一个学生列表
Stream<Student> passingStudents = students.stream(); // 将列表转换为流
-
使用
filter
方法筛选出成绩超过60分的学生,过滤条件为s -> s.getScore() > 60
。这样可以确保只有满足条件的元素才会继续参与后续操作。
List<Student> filteredStudents = passingStudents.filter(s -> s.getScore() > 60).collect(Collectors.toList()); // 使用filter方法筛选并收集结果
-
使用
collect(Collectors.toList())
将过滤后的流收集回列表,以便后续处理或输出。这一步确保了筛选结果得以保存。
-
最后,使用
forEach
方法打印出所有通过的学生信息。这样可以直观地展示筛选结果。
//经过filter方法筛选后,结果已经保存在filteredStudents列表中
filteredStudents.forEach(student -> System.out.println("学生姓名:" + student.getName() + ",成绩:" + student.getScore())); // 打印通过的学生信息
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
循环或者 List
的 set
方法。
与 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
判断来识别重复元素。当我们需要对自定义对象进行去重时,可以通过重写对象的hashCode
和equals
方法,确保按照我们期望的逻辑来判断对象是否相同,从而达到理想的去重效果。
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]