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的四个要素:supplieraccumulatorcombinerfinisher

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。

原文发布于微信公众号 - java达人(drjava)

原文发表时间:2018-01-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java编程

四道Java基础题,你能对几道?

如果这道题你能得出正确答案,并能了解其中的原理的话。说明你基础还可以。如果你的答案 是 true 和true的话,你的基础就有所欠缺了。

1.1K1
来自专栏PHP在线

PHP实现四种基本排序算法

许多人都说算法是程序的核心,算法的好坏决定了程序的质量。作为一个初级phper,虽然很少接触到算法方面的东西。但是对于基本的排序算法还是应该掌握的,它是程序开发...

37512
来自专栏前端小作坊

0.1+0.2=0.30000000000000004问题的探究

首先声明这不是bug,原因在与十进制到二进制的转换导致的精度问题!其次这几乎出现在很多的编程语言中:C/C++,Java,Javascript中,准确的说:“使...

911
来自专栏令仔很忙

深入理解HashMap(及hash函数的真正巧妙之处)

原文地址:http://www.iteye.com/topic/539465 Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就...

2941
来自专栏智能算法

Python学习(七)---- 面向对象学习(类)

原文地址: https://blog.csdn.net/fgf00/article/details/52449707 编辑:智能算法,欢迎关注!

1362
来自专栏编码前线

在java中String类为什么要设计成final?

String很多实用的特性,比如说“不可变性”,是工程师精心设计的艺术品!艺术品易碎!用final就是拒绝继承,防止世界被熊孩子破坏,维护世界和平!

952
来自专栏nummy

python抽象基类abc

python中并没有提供抽象类与抽象方法,但是提供了内置模块abc(abstract base class)来模拟实现抽象类。

1431
来自专栏成猿之路

Java面试题基础篇

1256
来自专栏华章科技

从Zero到Hero,一文掌握Python关键代码

首先,什么是 Python?根据 Python 创建者 Guido van Rossum 所言,Python 是一种高级编程语言,其设计的核心理念是代码的易读性...

843
来自专栏Golang语言社区

go(golang)中的类型转换

在使用 go 这样的强类型语言时,我们常常会遇到类型转换的问题。比如 int 类型转 int64,interface{} 转 struct ,对一种类型取指针、...

90710

扫码关注云+社区

领取腾讯云代金券