Java基础教程之新特性 · Stream流
1️⃣ 概念及特征2️⃣ 优势和缺点3️⃣ 使用3.1 语法3.2 常用API详解3.3 案例 4️⃣ 应用场景5️⃣ 使用技巧6️⃣ 并行流 ParallelStream🌾 总结
1️⃣ 概念及特征
Java的Stream流是在Java 8中引入的一种用于处理集合数据的功能强大且易于使用的工具,旨在简化集合框架的操作。它的设计目的是为了提供一种更简洁、更灵活和更可读的方式来处理集合数据。
在之前,我们通常使用迭代器或循环来遍历和操作集合元素,这种方式容易出错且代码冗长。Java 8通过引入Stream流来解决这个问题,提供了一种函数式编程风格的集合操作方法。
Stream流是对集合进行操作的高级抽象,可以将集合看作是一种源(source),而Stream表示这个源上进行的计算操作序列。 通过使用Stream API,我们可以以流水线方式处理数据,并进行各种转换和聚合操作。
在Java中,Stream流分为两种类型:
流(Stream):表示顺序流,按照数据源的顺序进行操作,适用于串行操作。并行流(ParallelStream):表示并行流,可以同时对数据源的多个元素进行操作,适用于并行计算。Stream流具有以下特点:
流是一次性的:流不会保存元素,它仅仅描述了操作的序列,并且在执行聚合操作之后就被消耗掉了。即使我们对一个流执行多个操作,每个操作也只会在需要输出结果时才会执行,并且在执行完毕后,流不能再次使用。这与传统的集合不同,集合可以随时进行增删元素的操作;流是无状态的:流的操作不会修改原始数据结构,而是通过创建一个新的流来执行操作,并最终返回一个结果。原始数据结构保持不变。这种无状态的特性使得流操作可以并行处理数据,不用担心多线程下的数据竞争问题;流是延迟执行的:流的操作被称为延迟执行,也就是说,在流的聚合操作被触发之前,中间操作不会立即执行。这意味着我们可以先构建一个复杂的流操作链,然后在需要结果的时候才触发最终的操作。这种延迟执行的机制有助于优化性能,避免不必要的计算。Stream流的实现原理主要基于迭代器和函数式编程的思想。在内部迭代的过程中,流通过一系列操作进行链式处理,将每个元素传递给下一个操作,并最终生成结果。
在并行流的情况下,流将输入数据分成多个小块,分配给不同的线程并行处理。处理完后,再合并结果并返回。
2️⃣ 优势和缺点
Stream流具有以下优点:
简洁:使用流的聚合操作可以极大地减少代码量;高效:流的并行操作可以利用多核处理器提高运行效率;函数式编程:流的操作方法遵循函数式编程的思想,使代码更加简洁、易读和可维护;可复用:可以使用复合操作将多个流操作链在一起。然而,Stream流也有一些缺点:
可读性降低:对于复杂的操作,使用Stream可能比传统的循环方式可读性稍差;一次性使用:一旦流被使用过,就不能再次使用,需要重新创建一个新的流;可能会影响性能:虽然并行流可以提高运行效率,但在某些情况下,额外的分组和合并操作可能会造成性能下降。3️⃣ 使用
3.1 语法
Stream提供了两种类型的操作:中间操作和终端操作。中间操作用于链式调用,并可以有多个,而终端操作是触发计算的地方。
而使用Stream主要分为三个步骤:
创建流:也即获取一个Stream对象,可以通过集合、数组或者其他方式创建一个Stream。如可以使用Stream.of()
方法创建流;进行中间操作:对Stream进行连续的中间操作,包括过滤、映射、排序、去重等处理。如可以使用forEach()
方法遍历流中的元素,并使用filter()
、map()
、sorted()
等方法对流进行操作;执行终结操作:最后使用一个终结操作来触发计算并产生结果,如收集、聚合、遍历等。如可以使用reduce()
方法进行元素的归约操作,使用collect()
方法进行元素的收集操作。3.2 常用API详解
Stream API提供了丰富的操作方法,可根据不同的需求灵活选择。常用的操作API有:
Intermediate操作:如filter()
、map()
、sorted()
,用于对元素进行筛选、映射、排序等操作。Terminal操作:如forEach()
、count()
、collect()
,用于对流进行最终的输出、统计和收集操作。Short-circuiting操作:如findFirst()
、anyMatch()
、allMatch()
,用于在满足条件时立即终止流的操作。 以下是一些Stream操作API详情列表:
类型 | 方法 | 作用 |
---|---|---|
中间操作 | filter(Predicate) | 过滤符合条件的元素 |
map(Function) | 对每个元素应用转换函数 | |
flatMap(Function) | 将每个元素转换成Stream对象,然后将所有的Stream连接成一个Stream | |
distinct() | 去除重复的元素 | |
sorted([Comparator]) | 排序元素,默认为自然排序 | |
limit(n) | 截取指定数量的元素 | |
skip(n) | 跳过指定数量的元素 | |
peek(Consumer) | 对每个元素执行操作,不影响流中的其他元素 | |
takeWhile(Predicate) | 从开头开始连续取元素满足指定条件,直到遇到不满足条件的元素 | |
dropWhile(Predicate) | 从开头开始连续跳过元素满足指定条件,直到遇到不满足条件的元素 | |
终结操作 | collect(Collector) | 将流转换为集合或其他数据结构 |
forEach(Consumer) | 遍历流中的元素,并对其执行操作 | |
reduce(BinaryOperator) | 使用给定的二元操作符将元素归约成一个值 | |
max([Comparator]) | 找出流中的最大值 | |
min([Comparator]) | 找出流中的最小值 | |
toArray() | 将流中的元素转换为数组 | |
count() | 统计流中的元素数量 | |
findFirst() | 返回满足条件的第一个元素 | |
findAny() | 返回任意满足条件的元素 | |
anyMatch(Predicate) | 判断流中是否存在任意一个元素满足给定条件 | |
allMatch(Predicate) | 判断流中所有元素是否都满足给定条件 | |
noneMatch(Predicate) | 判断流中是否没有任何元素满足给定条件 |
3.3 案例
下面是一个简单的Java程序,演示了上述所有方法的使用:
import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;public class StreamOperationsDemo { public static void main(String[] args) { // 创建一个包含整数的集合 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5); // filter: 过滤掉大于3的元素 List<Integer> filteredList = numbers.stream() .filter(num -> num <= 3) .collect(Collectors.toList()); System.out.println("Filtered List: " + filteredList); // map: 将每个元素乘以2 List<Integer> mappedList = numbers.stream() .map(num -> num * 2) .collect(Collectors.toList()); System.out.println("Mapped List: " + mappedList); // flatMap: 将每个元素转换成Stream对象,然后将所有的Stream连接成一个Stream List<String> words = Arrays.asList("Hello", "World"); List<String> flatMappedList = words.stream() .flatMap(word -> Arrays.stream(word.split(""))) .collect(Collectors.toList()); System.out.println("FlatMapped List: " + flatMappedList); // distinct: 去除重复的元素 List<Integer> distinctList = numbers.stream() .distinct() .collect(Collectors.toList()); System.out.println("Distinct List: " + distinctList); // sorted: 对元素进行排序 List<Integer> sortedList = numbers.stream() .sorted() .collect(Collectors.toList()); System.out.println("Sorted List: " + sortedList); // limit: 截取指定数量的元素 List<Integer> limitedList = numbers.stream() .limit(3) .collect(Collectors.toList()); System.out.println("Limited List: " + limitedList); // skip: 跳过指定数量的元素 List<Integer> skippedList = numbers.stream() .skip(3) .collect(Collectors.toList()); System.out.println("Skipped List: " + skippedList); // peek: 对每个元素执行操作,不影响流中的其他元素 List<Integer> peekedList = numbers.stream() .peek(num -> System.out.println("Peeking element: " + num)) .collect(Collectors.toList()); // takeWhile: 从开头开始连续取元素满足条件,直到遇到不满足条件的元素 List<Integer> takenList = numbers.stream() .takeWhile(num -> num < 4) .collect(Collectors.toList()); System.out.println("Taken List: " + takenList); // dropWhile: 从开头开始连续跳过元素满足条件,直到遇到不满足条件的元素 List<Integer> droppedList = numbers.stream() .dropWhile(num -> num < 4) .collect(Collectors.toList()); System.out.println("Dropped List: " + droppedList); // collect: 将流转换为集合或其他数据结构 List<Integer> collectedList = numbers.stream() .collect(Collectors.toList()); System.out.println("Collected List: " + collectedList); // forEach: 遍历流中的元素,并对其执行操作 numbers.stream() .forEach(System.out::println); // reduce: 使用给定的二元操作符将元素归约成一个值 int sum = numbers.stream() .reduce(0, Integer::sum); System.out.println("Sum: " + sum); // max: 找出流中的最大值 int max = numbers.stream() .max(Integer::compare) .orElse(-1); System.out.println("Max: " + max); // min: 找出流中的最小值 int min = numbers.stream() .min(Integer::compare) .orElse(-1); System.out.println("Min: " + min); // toArray: 将流中的元素转换为数组 Integer[] array = numbers.stream() .toArray(Integer[]::new); System.out.println("Array: " + Arrays.toString(array)); // count: 统计流中的元素数量 long count = numbers.stream() .count(); System.out.println("Count: " + count); // findFirst: 返回满足条件的第一个元素 int first = numbers.stream() .findFirst() .orElse(-1); System.out.println("First: " + first); // findAny: 返回任意满足条件的元素 int any = numbers.stream() .findAny() .orElse(-1); System.out.println("Any: " + any); // anyMatch: 判断流中是否存在任意一个元素满足给定条件 boolean anyMatch = numbers.stream() .anyMatch(num -> num % 2 == 0); System.out.println("Any Match: " + anyMatch); // allMatch: 判断流中所有元素是否都满足给定条件 boolean allMatch = numbers.stream() .allMatch(num -> num % 2 == 0); System.out.println("All Match: " + allMatch); // noneMatch: 判断流中是否没有任何元素满足给定条件 boolean noneMatch = numbers.stream() .noneMatch(num -> num > 10); System.out.println("None Match: " + noneMatch); }}
这个程序演示了如何使用Stream的中间操作和终端操作。
程序的运行结果如下:
Filtered List: [1, 2, 3, 1, 2, 3]Mapped List: [2, 4, 6, 8, 10, 2, 4, 6, 8, 10]FlatMapped List: [H, e, l, l, o, W, o, r, l, d]Distinct List: [1, 2, 3, 4, 5]Sorted List: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]Limited List: [1, 2, 3]Skipped List: [4, 5, 1, 2, 3, 4, 5]Peeking element: 1Peeking element: 2Peeking element: 3Peeking element: 4Peeking element: 5Peeking element: 1Peeking element: 2Peeking element: 3Peeking element: 4Peeking element: 5Taken List: [1, 2, 3]Dropped List: [4, 5, 1, 2, 3, 4, 5]Collected List: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]1234512345Sum: 30Max: 5Min: 1Array: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]Count: 10First: 1Any: 1Any Match: trueAll Match: falseNone Match: true
4️⃣ 应用场景
Stream流广泛应用于数据处理、集合操作、并行计算等场景。它可以使代码更简洁、易读和具有可维护性,同时充分发挥多核处理器的计算能力。如下:
数据分析:根据条件过滤出需要的数据,并进行统计、汇总或生成报表;数据处理:对大规模、复杂的数据集合进行筛选、转换、排序以及聚合和分组等;数据查询:通过多个中间操作构建复杂的查询条件,获取符合要求的数据;并行处理:在多核处理器上可利用parallel
方法实现并行处理大批量数据,提高系统的处理性能。 例如,对于一个电商平台的订单数据,我们可以使用流来实现以下功能:
筛选出所有金额大于1000的订单;将订单按照金额从高到低进行排序;获取前5个订单的信息;…5️⃣ 使用技巧
要使用流,首先需要从数据源创建一个流,然后通过一系列的中间操作和终端操作来对流进行处理和操作。
在使用流时,可以注意以下几点优化技巧:
合理选择流的类型,根据实际情况选择顺序流或并行流,对于大数据集合,考虑使用并行流以提高性能;尽量减少中间操作的数量,合并多个操作可以减少迭代次数;尽量避免使用短路操作,以充分发挥并行流的优势;使用延迟执行的特性,只在需要获取结果时触发终端操作。6️⃣ 并行流 ParallelStream
并行流(ParallelStream)允许在多线程环境下并发地执行操作,从而提高处理大数据集的效率。
ParallelStream类在Java中没有特有的方法。它与普通的Stream类具有相同的操作方法,可以使用 filter
、map
、flatMap
、distinct
、sorted
、limit
、skip
、peek
等方法。这些方法可以在并行流上执行,并发地处理数据。并行流会自动将数据分成多个部分,并在多个线程上同时进行处理,加快了处理速度。
需要注意的是,在使用并行流时,应该要注意线程安全和性能问题。如果并行执行的操作具有共享状态、副作用或依赖于元素之间的顺序,那么可能会导致不正确的结果。并行流适用于对大量元素进行计算密集型操作,但并不适用于有状态或依赖前后元素的操作。因此,在使用并行流时,需要确保操作的可靠性,并在必要时使用同步措施来保证线程安全。
除了以上普通的Stream操作方法,在并行流中还可以使用.parallel()
和.sequential()
方法切换并行流和顺序流的操作模式。.parallel()
方法将流转换为并行流,允许并发地对元素进行操作。而.sequential()
方法则将并行流转回为顺序流,仅使用单线程顺序地处理元素。
🌾 总结
Java Stream流为我们提供了一种简洁而强大的方式来操作数据集合。它具有许多优点,如简化操作、惰性求值和并行处理。同时也有一些缺点,如学习成本稍高和可读性稍差。然而,在正确使用和优化Stream的情况下,可以极大地提高代码的可读性和维护性,并实现更高效的数据处理与计算。
通过使用流,我们可以以更直观、简洁的方式对数据进行处理和操作,并发挥多核处理器的计算能力。
然而,使用流也需要注意数据量、性能和适用场景等因素。最重要的是根据具体情况选择合适的流类型,并根据实际需求合理组合流的操作,以实现更高效、可读性更好的代码。