Java 中文官方教程 2022 版(二十七)(1)https://developer.aliyun.com/article/1486846
以下示例使用 for-each 循环打印集合roster
中包含的所有成员的名称:
for (Person p : roster) { System.out.println(p.getName()); }
以下示例使用聚合操作forEach
打印出集合roster
中包含的所有成员:
roster .stream() .forEach(e -> System.out.println(e.getName());
尽管在此示例中,使用聚合操作的版本比使用 for-each 循环的版本更长,但您将看到,对于更复杂的任务,使用批量数据操作的版本将更简洁。
下面涵盖了以下主题:
- 管道和流
- 聚合操作和迭代器之间的区别
在示例BulkDataOperationsExamples
中找到本节描述的代码片段。
管道和流
管道是一系列聚合操作。以下示例使用由聚合操作filter
和forEach
组成的管道打印集合roster
中包含的男性成员:
roster .stream() .filter(e -> e.getGender() == Person.Sex.MALE) .forEach(e -> System.out.println(e.getName()));
将此示例与使用 for-each 循环打印集合roster
中的男性成员的示例进行比较:
for (Person p : roster) { if (p.getGender() == Person.Sex.MALE) { System.out.println(p.getName()); } }
一个管道包含以下组件:
- 源:这可以是集合、数组、生成器函数或 I/O 通道。在此示例中,源是集合
roster
。 - 零个或多个中间操作。中间操作,如
filter
,会生成一个新流。
流是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源头传递值。此示例通过调用stream
方法从集合roster
创建流。filter
操作返回一个包含与其谓词(该操作的参数)匹配的元素的新流。在这个示例中,谓词是 lambda 表达式e -> e.getGender() == Person.Sex.MALE
。如果对象e
的gender
字段的值为Person.Sex.MALE
,则返回布尔值true
。因此,在这个示例中,filter
操作返回一个包含集合roster
中所有男性成员的流。 - 终端操作。终端操作,比如
forEach
,产生一个非流结果,比如一个原始值(比如一个双精度值)、一个集合,或者在forEach
的情况下,根本没有值。在这个示例中,forEach
操作的参数是 lambda 表达式e -> System.out.println(e.getName())
,它在对象e
上调用getName
方法。(Java 运行时和编译器推断出对象e
的类型是Person
。)
以下示例计算了集合roster
中所有男性成员的平均年龄,使用了由filter
、mapToInt
和average
聚合操作组成的流水线:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
mapToInt
操作返回一个新的IntStream
类型的流(这是一个只包含整数值的流)。该操作将其参数中指定的函数应用于特定流中的每个元素。在这个示例中,函数是Person::getAge
,这是一个返回成员年龄的方法引用。(或者,你可以使用 lambda 表达式e -> e.getAge()
。)因此,在这个示例中,mapToInt
操作返回一个包含集合roster
中所有男性成员年龄的流。
average
操作计算IntStream
类型流中包含的元素的平均值。它返回一个OptionalDouble
类型的对象。如果流不包含任何元素,则average
操作返回一个空的OptionalDouble
实例,并调用getAsDouble
方法会抛出NoSuchElementException
。JDK 包含许多像average
这样返回通过组合流内容得到的一个值的终端操作。这些操作被称为归约操作;更多信息请参见归约部分。
聚合操作和迭代器之间的区别
聚合操作,如forEach
,看起来像迭代器。然而,它们有几个根本的区别:
- 它们使用内部迭代:聚合操作不包含像
next
这样的方法来指示它们处理集合的下一个元素。通过内部委托,您的应用程序确定要迭代的集合,但 JDK 确定如何迭代集合。通过外部迭代,您的应用程序确定要迭代的集合以及如何迭代它。然而,外部迭代只能按顺序迭代集合的元素。内部迭代没有这种限制。它可以更容易地利用并行计算,这涉及将问题分解为子问题,同时解决这些问题,然后将子问题的解决方案合并为结果。有关更多信息,请参见并行性部分。 - 它们从流中处理元素:聚合操作从流中处理元素,而不是直接从集合中处理。因此,它们也被称为流操作。
- 它们支持行为作为参数:您可以为大多数聚合操作指定 lambda 表达式作为参数。这使您可以自定义特定聚合操作的行为。
缩减
原文:
docs.oracle.com/javase/tutorial/collections/streams/reduction.html
章节聚合操作描述了以下操作流水线,它计算roster
集合中所有男性成员的平均年龄:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
JDK 包含许多终端操作(如average
、sum
、min
、max
和count
),它们通过组合流的内容返回一个值。这些操作称为缩减操作。JDK 还包含返回集合而不是单个值的缩减操作。许多缩减操作执行特定任务,比如找到值的平均值或将元素分组到类别中。然而,JDK 为您提供了通用的缩减操作reduce
和collect
,本节将详细描述这些操作。
本节涵盖以下主题:
- Stream.reduce 方法
- Stream.collect 方法
你可以在示例ReductionExamples
中找到本节中描述的代码片段。
Stream.reduce
方法
Stream.reduce
方法是一个通用的缩减操作。考虑以下流水线,它计算roster
集合中男性成员年龄的总和。它使用Stream.sum
缩减操作:
Integer totalAge = roster .stream() .mapToInt(Person::getAge) .sum();
将此与以下使用Stream.reduce
操作计算相同值的流水线进行比较:
Integer totalAgeReduce = roster .stream() .map(Person::getAge) .reduce( 0, (a, b) -> a + b);
在这个例子中,reduce
操作接受两个参数:
identity
:身份元素既是缩减的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0
;这是年龄总和的初始值,也是如果roster
集合中不存在成员时的默认值。accumulator
: 累加器函数接受两个参数:减少的部分结果(在这个例子中,到目前为止所有处理过的整数的总和)和流的下一个元素(在这个例子中,一个整数)。它返回一个新的部分结果。在这个例子中,累加器函数是一个 lambda 表达式,它将两个Integer
值相加并返回一个Integer
值:
(a, b) -> a + b
reduce
操作总是返回一个新值。然而,累加器函数在处理流的每个元素时也返回一个新值。假设你想将流的元素减少到一个更复杂的对象,比如一个集合。这可能会影响你的应用程序的性能。如果你的reduce
操作涉及将元素添加到一个集合中,那么每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。更新现有集合会更有效。你可以使用Stream.collect
方法来实现这一点,下一节将介绍。
collect
方法
与reduce
方法不同,它在处理元素时总是创建一个新值,collect
方法修改或改变了现有值。
考虑如何在流中找到值的平均值。你需要两个数据:值的总数和这些值的总和。然而,像reduce
方法和所有其他减少方法一样,collect
方法只返回一个值。你可以创建一个包含成员变量来跟踪值的总数和这些值总和的新数据类型,比如下面的类Averager
:
class Averager implements IntConsumer { private int total = 0; private int count = 0; public double average() { return count > 0 ? ((double) total)/count : 0; } public void accept(int i) { total += i; count++; } public void combine(Averager other) { total += other.total; count += other.count; } }
以下管道使用Averager
类和collect
方法来计算所有男性成员的平均年龄:
Averager averageCollect = roster.stream() .filter(p -> p.getGender() == Person.Sex.MALE) .map(Person::getAge) .collect(Averager::new, Averager::accept, Averager::combine); System.out.println("Average age of male members: " + averageCollect.average());
这个例子中的collect
操作接受三个参数:
supplier
: 供应商是一个工厂函数;它构造新实例。对于collect
操作,它创建结果容器的实例。在这个例子中,它是Averager
类的一个新实例。accumulator
: 累加器函数将流元素合并到结果容器中。在这个例子中,它通过将count
变量加一并将流元素的值(代表男性成员年龄的整数)加到total
成员变量中,修改了Averager
结果容器。combiner
:合并器函数接受两个结果容器并合并它们的内容。在这个例子中,它通过增加另一个Averager
实例的count
成员变量的值到Averager
结果容器的count
变量,并将另一个Averager
实例的total
成员变量的值加到total
成员变量中来修改Averager
结果容器。
请注意以下内容:
- 供应商是一个 lambda 表达式(或方法引用),与
reduce
操作中的单位元素等值相对。 - 累加器和合并器函数不返回值。
- 您可以在并行流中使用
collect
操作;有关更多信息,请参阅并行性部分。(如果您在并行流中运行collect
方法,那么当合并器函数创建一个新对象时,例如在这个例子中创建一个Averager
对象时,JDK 会创建一个新线程。因此,您不必担心同步。)
尽管 JDK 为您提供了average
操作来计算流中元素的平均值,但如果您需要从流的元素计算多个值,可以使用collect
操作和自定义类。
collect
操作最适合集合。以下示例使用collect
操作将男性成员的姓名放入集合中:
List<String> namesOfMaleMembersCollect = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .map(p -> p.getName()) .collect(Collectors.toList());
此版本的collect
操作接受一个类型为Collector
的参数。这个类封装了在collect
操作中用作参数的函数,该操作需要三个参数(供应商、累加器和合并器函数)。
Collectors
类包含许多有用的归约操作,例如将元素累积到集合中并根据各种标准对元素进行总结。这些归约操作返回Collector
类的实例,因此您可以将它们作为collect
操作的参数使用。
此示例使用Collectors.toList
操作,将流元素累积到一个新的List
实例中。与Collectors
类中的大多数操作一样,toList
操作符返回一个Collector
实例,而不是一个集合。
以下示例按性别对集合roster
的成员进行分组:
Map<Person.Sex, List<Person>> byGender = roster .stream() .collect( Collectors.groupingBy(Person::getGender));
groupingBy
操作返回一个映射,其键是应用作为其参数指定的 lambda 表达式(称为分类函数)的结果值。在这个例子中,返回的映射包含两个键,Person.Sex.MALE
和 Person.Sex.FEMALE
。键对应的值是包含通过分类函数处理时对应于键值的流元素的 List
实例。例如,对应于键 Person.Sex.MALE
的值是一个包含所有男性成员的 List
实例。
以下示例按性别检索集合 roster
中每个成员的名称并按性别分组:
Map<Person.Sex, List<String>> namesByGender = roster .stream() .collect( Collectors.groupingBy( Person::getGender, Collectors.mapping( Person::getName, Collectors.toList())));
在这个例子中,groupingBy
操作需要两个参数,一个分类函数和一个 Collector
实例。Collector
参数称为下游收集器。这是 Java 运行时应用于另一个收集器结果的收集器。因此,这个 groupingBy
操作使您能够对 groupingBy
操作符创建的 List
值应用 collect
方法。这个例子应用了收集器 mapping
,它将映射函数 Person::getName
应用于流的每个元素。因此,结果流只包含成员的名称。包含一个或多个下游收集器的管道,像这个例子一样,称为多级减少。
以下示例检索每个性别成员的总年龄:
Map<Person.Sex, Integer> totalAgeByGender = roster .stream() .collect( Collectors.groupingBy( Person::getGender, Collectors.reducing( 0, Person::getAge, Integer::sum)));
reducing
操作需要三个参数:
identity
:类似于Stream.reduce
操作,身份元素既是减少的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0
;这是年龄总和的初始值,如果没有成员存在,则是默认值。mapper
:reducing
操作将此映射函数应用于所有流元素。在这个例子中,映射器检索每个成员的年龄。operation
:操作函数用于减少映射值。在这个例子中,操作函数添加Integer
值。
以下示例检索每个性别成员的平均年龄:
Map<Person.Sex, Double> averageAgeByGender = roster .stream() .collect( Collectors.groupingBy( Person::getGender, Collectors.averagingInt(Person::getAge)));
并行性
原文:
docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
并行计算涉及将问题分解为子问题,同时解决这些问题(并行执行,每个子问题在单独的线程中运行),然后将这些子问题的解决方案合并。Java SE 提供了分支/合并框架,它使您能够更轻松地在应用程序中实现并行计算。然而,使用此框架时,您必须指定如何将问题细分(分区)。使用聚合操作,Java 运行时为您执行此分区和解决方案的合并。
在使用集合的应用程序中实现并行性的一个困难在于集合不是线程安全的,这意味着多个线程不能在不引入线程干扰或内存一致性错误的情况下操作集合。集合框架提供了同步包装器,它可以为任意集合添加自动同步,使其线程安全。然而,同步会引入线程争用。你应该避免线程争用,因为它会阻止线程并行运行。聚合操作和并行流使你能够在非线程安全的集合上实现并行性,前提是在操作集合时不修改它。
请注意,并行性并不自动比串行执行操作更快,尽管在有足够数据和处理器核心的情况下可能会更快。虽然聚合操作使您更容易实现并行性,但确定您的应用程序是否适合并行性仍然是您的责任。
本节涵盖以下主题:
- 并行执行流
- 并发归约
- 顺序
- 副作用
- 懒惰性
- 干扰
- 有状态的 Lambda 表达式
你可以在示例ParallelismExamples
中找到本节中描述的代码摘录。
并行执行流
你可以串行或并行执行流。当流并行执行时,Java 运行时将流分成多个子流。聚合操作并行迭代和处理这些子流,然后将结果合并。
当您创建一个流时,除非另有说明,它总是一个串行流。要创建一个并行流,请调用操作Collection.parallelStream
。或者,调用操作BaseStream.parallel
。例如,以下语句计算所有男性成员的平均年龄:
double average = roster .parallelStream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
并发减少
再次考虑以下示例(在减少部分中描述),该示例按性别对成员进行分组。此示例调用collect
操作,将集合roster
减少为Map
:
Map<Person.Sex, List<Person>> byGender = roster .stream() .collect( Collectors.groupingBy(Person::getGender));
以下是并行等价的:
ConcurrentMap<Person.Sex, List<Person>> byGender = roster .parallelStream() .collect( Collectors.groupingByConcurrent(Person::getGender));
这被称为并发减少。如果包含collect
操作的特定管道满足以下所有条件,则 Java 运行时执行并发减少:
- 流是并行的。
collect
操作的参数,收集器,具有特征Collector.Characteristics.CONCURRENT
。要确定收集器的特征,请调用Collector.characteristics
方法。- 流要么是无序的,要么收集器具有特征
Collector.Characteristics.UNORDERED
。要确保流是无序的,请调用BaseStream.unordered
操作。
注意:此示例返回一个ConcurrentMap
的实例,而不是Map
,并调用groupingByConcurrent
操作,而不是groupingBy
。(有关ConcurrentMap
的更多信息,请参见并发集合部分。)与操作groupingByConcurrent
不同,操作groupingBy
在并行流中表现不佳。(这是因为它通过键合并两个映射,这在计算上是昂贵的。)同样,操作Collectors.toConcurrentMap
在并行流中的性能优于操作Collectors.toMap
。
排序
流水线处理流的元素的顺序取决于流是串行执行还是并行执行、流的来源以及中间操作。例如,考虑以下示例,该示例多次使用forEach
操作打印ArrayList
实例的元素:
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 }; List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray)); System.out.println("listOfIntegers:"); listOfIntegers .stream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("listOfIntegers sorted in reverse order:"); Comparator<Integer> normal = Integer::compare; Comparator<Integer> reversed = normal.reversed(); Collections.sort(listOfIntegers, reversed); listOfIntegers .stream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("Parallel stream"); listOfIntegers .parallelStream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("Another parallel stream:"); listOfIntegers .parallelStream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("With forEachOrdered:"); listOfIntegers .parallelStream() .forEachOrdered(e -> System.out.print(e + " ")); System.out.println("");
该示例包含五个流水线。它打印类似以下的输出:
listOfIntegers: 1 2 3 4 5 6 7 8 listOfIntegers sorted in reverse order: 8 7 6 5 4 3 2 1 Parallel stream: 3 4 1 6 2 5 7 8 Another parallel stream: 6 3 1 5 7 8 4 2 With forEachOrdered: 8 7 6 5 4 3 2 1
该示例执行以下操作:
- 第一个流水线按照它们添加到列表中的顺序打印
listOfIntegers
列表的元素。 - 第二个流水线在使用
Collections.sort
方法对listOfIntegers
进行排序后打印元素。 - 第三和第四个流水线以一种看似随机的顺序打印列表的元素。请记住,流操作在处理流的元素时使用内部迭代。因此,当您并行执行流时,除非流操作另有规定,否则 Java 编译器和运行时会确定处理流元素的顺序,以最大化并行计算的好处。
- 第五个流水线使用
forEachOrdered
方法,该方法按照其来源指定的顺序处理流的元素,无论您是串行还是并行执行流。请注意,如果您在并行流中使用类似forEachOrdered
的操作,可能会丧失并行性的好处。
副作用
如果一个方法或表达式除了返回或产生一个值外,还修改了计算机的状态,那么它就具有副作用。例如,可变归约(使用collect
操作的操作;有关更多信息,请参见 Reduction 部分)以及调用System.out.println
方法进行调试。JDK 在流水线中处理某些副作用很好。特别是,collect
方法被设计为以并行安全的方式执行具有副作用的最常见流操作。forEach
和peek
等操作是为了副作用而设计的;一个返回 void 的 Lambda 表达式,比如调用System.out.println
的 Lambda 表达式,除了具有副作用外什么也做不了。即便如此,你应该谨慎使用forEach
和peek
操作;如果你在并行流中使用其中一个操作,那么 Java 运行时可能会从多个线程同时调用你指定为参数的 Lambda 表达式。此外,永远不要在filter
和map
等操作中传递具有副作用的 Lambda 表达式作为参数。接下来的章节讨论干扰和有状态的 Lambda 表达式,它们都可能是副作用的来源,并且可能返回不一致或不可预测的结果,特别是在并行流中。然而,首先讨论懒惰的概念,因为它对干扰有直接影响。
懒惰
所有中间操作都是懒惰的。如果一个表达式、方法或算法只有在需要时才会被评估,那么它就是懒惰的(如果算法立即被评估或处理,则是急切的)。中间操作是懒惰的,因为它们直到终端操作开始才开始处理流的内容。懒惰地处理流使得 Java 编译器和运行时能够优化它们处理流的方式。例如,在像filter
-mapToInt
-average
这样的流水线中,average
操作可以从mapToInt
操作创建的流中获取前几个整数,而这些整数是从filter
操作获取的。average
操作会重复这个过程,直到它从流中获取了所有需要的元素,然后计算平均值。
Java 中文官方教程 2022 版(二十七)(3)https://developer.aliyun.com/article/1486852