Java 8 Stream 教程 (二)
作者: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); // finisher
String 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 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))));
现在我们有包含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。
java达人
ID:drjava
(扫码或长按识别)
- 【Yui宝宝小课堂】1000----单挑教程:慢打
- 看30年前的楷书教程,非常走心!
- 【常识】岛国良心教程:论面瘫如何迅速成为交际花?
- 二十九,蒸馒头,花式馒头教程在此,拿走不谢!
- 【视频】十二生肖挂件的刺绣教程与针法图解
- 《绝地求生》外挂辅助免费下载 源代码轻松吃鸡教程
- Java NIO:Buffer、Channel 和 Selector
- 使用 JavaScript 实现机器学习和神经学网络
- 教程 | 牢记这3点,业余赛场上踢边后卫也能出彩
- 绝地求生刺激战场:估计这些教程好多人都没看吧!