作者:Benjamin
译者:java达人
来源:http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/(点击阅读原文前往)
高级操作
stream支持各种不同的操作。我们已经了解了最重要的操作,如filter或map Java 8 Stream 教程 (一) 。您可以学习其他的操作(参考Stream Javadoc)。我们将更深入地了解复杂的操作,collect,flatMap和 reduce。
本节的大部分代码示例使用下面 person组成的list进行演示:
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是一种非常有用的终端操作,可以将stream元素转换为不同类型的结果,例如List, Set or Map。 Collect 接受一个包含四个不同操作的Collector: supplier, accumulator, combiner 和 finisher。这听起来很复杂,优点是Java 8通过Collector类支持各种内置收集器。因此,对于最常见的操作,您不必自己实现Collector。
让我们从一个十分常见的用例开始:
List<Person> filtered =
persons .stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList()); System.out.println(filtered); // [Peter, Pamela]
正如您所看到的,根据stream的元素构建list 非常简单。如果需要set而不是list 使用Collectors.toSet()就可以。
下一个例子将所有人按年龄分组:
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]
Collectors 是多功能的。您还可以在stream的元素上创建聚合,例如计算平均年龄:
Double averageAge = persons .stream()
.collect(Collectors.averagingInt(p -> p.age));System.out.println(averageAge); // 19.0
如果您对更全面的统计数据感兴趣,汇总collectors返回一个专门的内置汇总统计对象。因此,我们可以简单地确定年龄最小值、最大值和算术平均值以及总和和数量。
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}
下一个示例将所有persons 合并成一个字符串:
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.
join collector 接收一个分隔符以及可选的前缀和后缀。
为了将stream元素转换为map,我们必须指定键和值如何映射。请记住,映射的键必须是惟一的,否则会抛出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}
现在我们知道了一些最强大的内置collector,让我们尝试构建自专用的collector。我们想要将stream中所有person转换成一个由|管道字符分隔的大写字母组成的字符串。为了实现这一点,我们通过collector. of()创建了一个新的collector。我们必须传递collector的四个要素:supplier、accumulator、 combiner和finisher。
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); // finisherString names = persons .stream()
.collect(personNameCollector);System.out.println(names); // MAX | PETER | PAMELA | DAVID
由于Java中的字符串是不可变的,所以我们需要一个类似StringJoiner的helper类来让collector构造我们的字符串。 supplier最初使用适当的分隔符构造了这样一个StringJoiner。 accumulator用于将每个人的大写名称添加到StringJoiner中。 combiner 知道如何将两个StringJoiners合并成一个。在最后一步中,finisher从StringJoiner中构造所需的字符串。
FlatMap
我们已经学习了如何利用map操作将stream的对象转换为另一种类型的对象。Map是有局限的,因为每个对象只能映射到一个对象。但是,如果我们想要将一个对象变换为多个对象,或者将它变换成根本不存在的对象呢?这就是flatMap发挥作用的地方。
FlatMap将stream的每个元素转换到其他对象的stream。因此,每个对象将被转换为零个、一个或多个基于stream的不同对象。这些stream的内容将被放置到flatMap操作的返回stream中。
在我们看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;
}}
接下来,我们利用stream相关知识实例化几个对象:
List<Foo> foos = new ArrayList<>();// create foosIntStream .range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i)));// create barsfoos.forEach(f ->
IntStream .range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));
现在我们有包含3个foos的list,每个foo都包含三个bars。
FlatMap接受一个函数,该函数必须返回对象stream。为了处理每个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对象的stream转换成9个bar对象的stream。
最后,上述代码示例可以简化为stream操作的单管道:
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,必须添加多个空检查以防止可能的nullpointerexception:
Outer outer = new Outer();if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);}
利用optionals 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,不存在,则返回空的Optional。
Reduce
reduce操作将stream的所有元素合并到一个结果中。Java 8支持三种不同的reduce方法。第一种将stream中元素reduce为一个。让我们看看如何使用这个方法来确定最年长的人:
persons .stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela
reduce方法接受一个BinaryOperator累加器函数。这实际上是一个BiFunction,在这个例子中,两个操作数都有相同的类型Person。 BiFunctions类似于Function,但接受两个参数。示例函数比较两个人的年龄,以返回年龄最大的人。
第二个reduce方法接受 实体值和BinaryOperator累加器。该方法可用于构建一个新的Person,它聚合来自于stream的其他人的的姓名和年龄:
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类型,所以我们可以确定所有人的年龄和:
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
可以看到, accumulator函数完成所有工作。它首次被调用时初始值为0,第一个人是Max。在接下来的三个步骤中,sum持续增加到76的总年龄。
combiner 从未被调用?通过并行执行同样的stream程序可以解释这个秘密:
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;
});// 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
并行执行此stream将产生完全不同的执行过程。现在这个combiner被调用了。由于accumulator是并行调用的,所以需要combiner来汇总分离的累计值。
我们在下一节深入研究并行stream。