介绍
在编程领域,编写简洁、可读且高效的代码的艺术是一门经过时间磨练的技艺。促进这一点的工具之一是 Java 8 中引入的 Java Stream API。该 API 为我们处理集合的方式带来了范式转变,提供了一种比传统命令式风格更具声明性的方法。
在这篇文章中,我们将深入研究 Java Stream API 的变革力量,揭示每个 Java 开发人员都应该了解的其功能、用例和细微差别。
Java Stream API 简介
Java 8 中引入的 Java Stream API 代表了 Java 语言及其核心库最具变革性的补充之一。它不仅仅是一组新方法或实用程序,而是一种范式转变,鼓励开发人员采用功能性方法来处理数据。在我们继续之前,有必要了解为什么这样的 API 是必要的,以及它如何从根本上改变 Java 开发人员操作集合的方式。
历史背景
从历史上看,Java 主要是一种命令式编程语言。这意味着编码器明确地阐明了计算机为实现所需结果而必须采取的每个步骤。虽然这种方法很直接,但通常会产生冗长的代码,尤其是在对集合执行操作时。
在 Java 8 之前,使用集合通常意味着使用 for 循环、迭代器或 for-each 构造。这些不仅冗长而且缺乏表达性,常常导致代码背后的意图被埋藏在迭代机制之下。
函数式编程的兴起
随着函数式编程语言和范例的日益流行,Java 显然需要不断发展。函数式编程强调将计算表达为数学函数的评估,并避免改变状态或可变数据。这种方法可以使代码更加简洁、可预测和可维护。
Java 中的 Stream API 正是出于这种需求而诞生,它将函数式编程范式与传统的命令式 Java 融合在一起,允许开发人员以更具表现力和简洁的方式处理数据。
什么是流?
本质上,Java 中的 Stream 表示可以并行或顺序处理的元素序列(通常来自集合)。值得注意的是,流与集合不同,它不是数据结构。他们不存储数据。相反,它们传递数据,允许您在数据源上定义多个操作,这些操作可以按需计算,并且通常以优化的方式进行。
流可以是有限的或无限的。有限流具有固定数量的元素,就像从标准集合派生的流一样。相反,无限流没有固定的大小,根据给定的种子元素和函数动态生成其元素。
流如何改变运营
Stream API 的核心功能是能够将数据操作从外部迭代转换为内部迭代。外部迭代是开发人员在 Java 8 之前所做的事情——使用循环手动控制迭代。另一方面,内部迭代抽象了迭代过程,让库进行控制,这可以带来更优化的迭代。
例如,想象一下必须过滤和转换数字列表。使用 Stream API,您可以可视化通过一系列管道传递列表,其中每个管道代表一个操作,例如过滤或转换。该操作链可根据需要任意长或短,并且数据无缝地流经其中。
流的常见操作
Stream API 的优点在于它提供了多种可以链接在一起以形成复杂数据操作的操作。这些操作可大致分为中间操作和终端操作。中间操作返回一个流并且可以链接在一起,而终端操作会产生结果或副作用。
过滤和映射
过滤是数据处理中最基本的操作之一。它允许您根据给定条件或谓词有选择地从流中选取元素。
例子:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");"Alice", "Bob", "Charlie", "David"); List<String> namesStartingWithA = names.stream() .filter(n -> n.startsWith("A")) .collect(Collectors.toList());
另一方面,映射是关于转换流中的每个元素。当您想要将元素从一种类型转换为另一种类型或修改其状态时,此操作特别有用。
例子:
List<Integer> nameLengths = names.stream() .map(String::length) .collect(Collectors.toList());
聚合
聚合操作有助于将流压缩为单个汇总结果。Stream API 提供了sum、average、count和 等方法reduce。
例如,用于reduce连接字符串:
String concatenatedNames = names.stream() .reduce("", (name1, name2) -> name1 + " " + name2);
排序
Stream API 有助于通过该sorted方法进行排序。您可以使用自然排序或提供自定义比较器。
示例,按长度对名称进行排序:
List<String>sortedByLength=names.stream() .sorted(Comparator.comparingInt(String::length)) .collect(Collectors.toList());
独特和限制
有时,您可能需要消除重复项或限制流中的结果数量。
- distinct删除重复值:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4);1, 2, 2, 3, 3, 3, 4, 4); List<Integer> uniqueNumbers = numbers.stream() .distinct() .collect(Collectors.toList());
- limit限制结果的大小:
List<String> firstTwoNames = name.stream() .limit(2)2 ) .collect(Collectors.toList());
映射
flatMap是一种特殊的操作,可以通过“展平”结构将流的每个元素转换为零个或多个元素。在处理集合流时它特别有用。
例如,在字符串列表中查找唯一字符:
List<String> listOfWords = Arrays.asList("Hello", "World");"Hello", "World"); List<String> uniqueChars = listOfWords.stream() .map(w -> w.split("")) .flatMap(Arrays::stream) .distinct() .collect(Collectors.toList());
流中的惰性求值
惰性求值是 Java Stream API 最强大但不太直观的功能之一。要理解它的意义,我们首先要掌握Stream API中中间操作和终端操作的区别。
中间操作与终端操作
Java Streams 操作分为两种主要类型:
- 中间操作:这些操作返回另一个流并在流管道上设置新操作。示例包括filter、map和sorted。
- 终端操作:这些操作会产生结果或副作用,导致流管道被处理。示例包括collect、forEach和reduce。
延迟执行的力量
惰性求值是指将实际计算推迟到绝对必要时才进行。在流的上下文中,这意味着中间操作在调用时不会处理数据。相反,他们在流管道上设置一个新操作并等待。实际计算仅在调用终端操作时发生。
这种行为有几个好处:
- 性能优化:由于数据在需要时才进行处理,因此您可以避免不必要的计算,尤其是在涉及链式操作时。
- 短路:某些操作(例如findFirst或anyMatch)不需要处理整个数据集即可产生结果。通过惰性求值,一旦找到结果,处理就会停止。
例如,考虑一个流管道,它过滤掉偶数,然后找到第一个大于 5 的数字:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Optional<Integer> result = numbers.stream() .filter(n -> n % 2 == 0) .filter(n -> n > 5) .findFirst();
在这里,即使列表有数百万个数字,只要发现第一个大于 5 的偶数,流就会停止处理。这是由于中间操作的惰性本质和 的短路行为findFirst。
无限流
惰性求值还使得处理无限流成为可能。由于计算是延迟的,因此您可以定义具有无限源的流,但只要限制对其执行的操作,就不会遇到问题。
例如,使用该Stream.iterate方法,可以创建无限的偶数流:
Stream<Integer> infiniteEvens = Stream.iterate(0, n -> n + 2);0, n -> n + 2);
但是,如果您希望从此流中收集前 10 个偶数,则可以在不处理整个无限源的情况下执行此操作:
List<Integer> firstTenEvens = infiniteEvens.limit(10).collect(Collectors.toList());10).collect(Collectors.toList());
使用流进行并行处理
轻松并行化数据操作的能力是 Java Stream API 的突出功能之一。随着多核处理器的可用性不断提高,并行处理对于充分利用现代硬件的能力变得至关重要。值得庆幸的是,Stream API 提供了一种直观的机制来利用这种潜力。
引入并行流
并行流将数据分割成多个块,每个块由单独的线程处理。这种并发处理可以显着提高 CPU 密集型任务的性能,尤其是在处理大型数据集时。
创建并行流非常简单。您可以使用该方法将常规流转换为并行流parallel(),或者使用以下方法直接从集合中创建一个流parallelStream():
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Using parallel() Stream<Integer> parallelStream1 = numbers.stream().parallel(); // Using parallelStream() Stream<Integer> parallelStream2 = numbers.parallelStream();
底层:Fork/Join 框架
Java 的并行流利用 Java 7 中引入的 Fork/Join 框架。该框架旨在并行化递归任务,有效地使用工作线程池。Stream API 将数据划分为更小的块,并将它们分布在 Fork/Join 池中的可用线程之间以进行并发处理。
优点和注意事项
虽然并行处理可以显着提高速度,但这并不是灵丹妙药。需要记住的一些注意事项:
- 开销:并行性由于任务分解、线程管理和结果组合而引入开销。对于小型数据集或任务,这种开销可能会超过好处,使得并行版本比顺序版本慢。
- 有状态操作:有状态 lambda 表达式(在调用之间维护状态的表达式)在并行流中使用时可能会导致不可预测的结果。最好确保操作是无状态的并且没有副作用。
- 排序:并行处理可能不会维持原始数据的顺序,尤其是在map或等操作期间filter。如果顺序很重要,则可能会降低并行性的有效性,因为需要额外的步骤来维护它。
- 共享数据结构:使用共享可变数据结构可能会导致数据损坏或并发问题。建议使用并发数据结构或完全避免共享可变数据。
一个实际的例子
考虑一个场景,您想要计算一个大列表中每个数字的平方:
List<Integer> numbers = /* ... a large list ... */;/* ... a large list ... */; List<Integer> squares = numbers.parallelStream() .map(n -> n * n) .collect(Collectors.toList());
仅通过使用parallelStream(),任务就会自动拆分并同时处理,从而可能显着提高速度,尤其是对于较大的列表。
结论
Java Stream API 代表了 Java 作为一种编程语言的发展中的重大进步。它提倡函数式编程风格,从而产生更简洁、可读且通常更高效的代码。通过利用其延迟评估和并行处理等功能,开发人员可以针对数据处理挑战制定优化且优雅的解决方案。