前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 8 Stream 教程 (二)

Java 8 Stream 教程 (二)

作者头像
java达人
发布2018-01-31 12:34:24
7000
发布2018-01-31 12:34:24
举报
文章被收录于专栏:java达人

作者: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进行演示:

代码语言:javascript
复制
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。

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

代码语言:javascript
复制
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()就可以。

下一个例子将所有人按年龄分组:

代码语言:javascript
复制
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的元素上创建聚合,例如计算平均年龄:

代码语言:javascript
复制
Double averageAge = persons      .stream()
    .collect(Collectors.averagingInt(p -> p.age));System.out.println(averageAge);     // 19.0

如果您对更全面的统计数据感兴趣,汇总collectors返回一个专门的内置汇总统计对象。因此,我们可以简单地确定年龄最小值、最大值和算术平均值以及总和和数量。

代码语言:javascript
复制
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 合并成一个字符串:

代码语言:javascript
复制
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。您可以将合并函数作为额外参数传递,以绕过异常:

代码语言:javascript
复制
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

代码语言:javascript
复制
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之前,我们需要一个合适的类型层次结构:

代码语言:javascript
复制
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相关知识实例化几个对象:

代码语言:javascript
复制
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对象,我们只需传递适当的函数:

代码语言:javascript
复制
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操作的单管道:

代码语言:javascript
复制
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机检查。

有这样一个层次分明的结构:

代码语言:javascript
复制
class Outer {
    Nested nested;}class Nested {
    Inner inner;}class Inner {
    String foo;}

为了处理外部实例的内部字符串foo,必须添加多个空检查以防止可能的nullpointerexception:

代码语言:javascript
复制
Outer outer = new Outer();if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);}

利用optionals flatMap操作可以达到相同效果:

代码语言:javascript
复制
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为一个。让我们看看如何使用这个方法来确定最年长的人:

代码语言:javascript
复制
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的其他人的的姓名和年龄:

代码语言:javascript
复制
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类型,所以我们可以确定所有人的年龄和:

代码语言:javascript
复制
Integer ageSum = persons     .stream()
    .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);System.out.println(ageSum);  // 76

你可以看到结果是76,但在底层到底发生了什么?我们通过一些调试输出来扩展上面的代码:

代码语言:javascript
复制
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程序可以解释这个秘密:

代码语言:javascript
复制
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。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档