Java 8 Stream高级操作

时间:2023-03-17 11:40:23

Streams支持大量不同的操作。我们已经了解了最重要的操作,如​​filter​​​,​​map​​​。发现所有其他可用的操作(参见​​Stream Javadoc​​​)。我们深入研究更复杂的操作​​collect​​​,​​flatMap​​​,​​reduce​​。

本节中的大多数代码示例使用以下人员列表进行演示:


class Person {
String name;
int age;

Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return name;
}
}

List<Person> persons =
Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12));

Collect


Collect是一个非常有用的终端操作,以流的元素转变成一种不同的结果,例如一个List,Set或Map。Collect接受Collector包含四种不同操作的操作:供应商,累加器,组合器和修整器。这听起来非常复杂,但是Java 8通过Collectors类支持各种内置收集器。因此,对于最常见的操作,您不必自己实现收集器。

让我们从一个非常常见的用例开始:


List<Person> filtered =
persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList());

System.out.println(filtered);

代码输出:


[Peter, Pamela]

正如您所看到的,流的元素构造列表非常简单。需要一个集合而不是列表 - 只需使用​​Collectors.toList()​​。

下一个示例按年龄对所有人进行分组:


Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age));

personsByAge
.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));

代码产出


age 18: [Max]
age 23: [Peter, Pamela]
age 12: [David]

您还可以在流的元素上创建聚合,例如,确定所有人的平均年龄:


Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age));

System.out.println(averageAge);

代码产出


19.0

如果您对更全面的统计信息感兴趣,汇总收集器将返回一个特殊的内置摘要统计信息对象。因此,我们可以简单地确定人的最小,最大和算术平均年龄以及总和和计数。


IntSummaryStatistics ageSummary =
persons
.stream()
.collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary);

代码产出


IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

下一个示例将所有人连接成一个字符串:


String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));

System.out.println(phrase);

代码产出


In Germany Max and Peter and Pamela are of legal age.

Collect接受分隔符以及可选的前缀和后缀。

为了将流元素转换为映射,我们必须指定如何映射键和值。请记住,映射的键必须是唯一的,否则抛出一个​​IllegalStateException​​。您可以选择将合并函数作为附加参数传递以绕过异常:


Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));

System.out.println(map);

代码产出


{18=Max, 23=Peter;Pamela, 12=David}

现在我们知道了一些强大的Collect,让我们尝试构建我们自己的特殊Collect。我们希望将流的所有人转换为单个字符串,该字符串由|管道字符分隔的大写字母组成。为了实现这一目标,我们创建了一个新的​​Collector.of()​​。


Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher

String names = persons
.stream()
.collect(personNameCollector);

System.out.println(names);// MAX | PETER | PAMELA | DAVID

由于Java中的字符串是不可变的,我们需要一个帮助类StringJoiner,让Collect构造我们的字符串。供应商最初使用适当的分隔符构造这样的StringJoiner。累加器用于将每个人的大写名称添加到StringJoiner。组合器知道如何将两个StringJoiners合并为一个。在最后一步中,整理器从StringJoiner构造所需的String。

FlatMap


我们已经学会了如何利用​​map​​操作将流的对象转换为另一种类型的对象。Map有点受限,因为每个对象只能映射到另一个对象。但是如果我们想要将一个对象转换为多个其他对象或者根本不转换它们呢?这是flatMap救援的地方。

FlatMap将流的每个元素转换为其他对象的流。因此,每个对象将被转换为由流支持的零个,一个或多个其他对象。然后将这些流的内容放入返回flatMap操作流中。

在我们看到flatMap实际操作之前,我们需要一个适当的类型层


class Foo {
String name;
List<Bar> bars = new ArrayList<>();

Foo(String name) {
this.name = name;
}
}

class Bar {
String name;

Bar(String name) {
this.name = name;
}
}

接下来,我们利用有关流的知识来实例化几个对象:


List<Foo> foos = new ArrayList<>();

// create foos
IntStream
.range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i)));

// create bars
foos.forEach(f ->
IntStream
.range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));

现在我们列出了三个foos,每个foos由三个数据组成。

FlatMap接受一个必须返回对象流的函数。所以为了解决每个foo的bar对象,我们只传递相应的函数:


foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));

代码产出


Bar1 <- Foo1
Bar2 <- Foo1
Bar3 <- Foo1
Bar1 <- Foo2
Bar2 <- Foo2
Bar3 <- Foo2
Bar1 <- Foo3
Bar2 <- Foo3
Bar3 <- Foo3

如您所见,我们已成功将三个foo对象的流转换为九个bar对象的流。

最后,上面的代码示例可以简化为流操作的单个管道:


IntStream.range(1, 4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(f -> IntStream.range(1, 4)
.mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));

FlatMap也可用于Java 8中引入的Optional类。Optionals flatMap操作返回另一种类型的可选对象。因此,它可以用来防止令人讨厌的null检查。

这样一个高度分层的结构:


class Outer {
Nested nested;
}

class Nested {
Inner inner;
}

class Inner {
String foo;
}

为了解析foo外部实例的内部字符串,您必须添加多个空值检查以防止可能的NullPointerExceptions:


Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}

利用选项flatMap操作可以获得相同的行为:


Optional.of(new Outer())
.flatMap(o -> Optional.ofNullable(o.nested))
.flatMap(n -> Optional.ofNullable(n.inner))
.flatMap(i -> Optional.ofNullable(i.foo))
.ifPresent(System.out::println);

每个调用flatMap返回一个Optional包装所需对象(如果存在)或null不存在。

Reduce


Reduce操作将流的所有元素组合成单个结果。Java 8支持三种不同的reduce方法。第一个将元素流简化为流的一个元素。让我们看看我们如何使用这种方法来确定最老的人:


persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela

reduce方法接受一个BinaryOperator累加器函数。这实际上是一个双函数,两个操作数共享同一类型,在这种情况下是Person。双函数类似于函数,但接受两个参数。示例函数比较两个人的年龄,以返回年龄最大的人。

第二种reduce方法接受标识值和BinaryOperator累加器。此方法可用于构造一个新的Person,其中包含来自流中所有其他人的聚合名称和年龄:


Person result =
persons
.stream()
.reduce(new Person("", 0), (p1, p2) -> {
p1.age += p2.age;
p1.name += p2.name;
return p1;
});

System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76

第三种reduce方法接受三个参数:标识值,BiFunction累加器和类型的组合器函数BinaryOperator。由于身份值类型不限于Person类型,我们可以利用reduce来确定所有人的年龄总和:


Integer ageSum = persons
.stream()
.reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);

System.out.println(ageSum); // 76

正如你所看到的结果是76,但是究竟发生了什么?让我们通过一些调试输出扩展上面的代码:


Integer ageSum = persons
.stream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});

代码产出


accumulator: sum=0; person=Max
accumulator: sum=18; person=Peter
accumulator: sum=41; person=Pamela
accumulator: sum=64; person=David

正如你所看到的,累加器函数完成了所有的工作。它首先以初始恒等值0和第一个person Max被调用。在接下来的三个步骤中,总和随着最后一个步骤的年龄不断增加,人的总年龄达到76岁。

为什么组合器永远不会被调用?并行执行相同的流将解除秘密:


Integer ageSum = persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});

代码产出

Java 8 Stream高级操作

accumulator: sum=0; person=Pamela
accumulator: sum=0; person=David
accumulator: sum=0; person=Max
accumulator: sum=0; person=Peter
combiner: sum1=18; sum2=23
combiner: sum1=23; sum2=12
combiner: sum1=41; sum2=35

并行执行此流会导致完全不同的执行行为。现在实际上调用了组合器。由于累加器是并行调用的,因此需要组合器来对各个累加值求和。