资讯专栏INFORMATION COLUMN

Java 8 Strem高级操作

dadong / 2396人阅读

摘要:接受包含四种不同操作的操作供应商,累加器,组合器和修整器。累加器用于将每个人的大写名称添加到。第二种方法接受标识值和累加器。由于累加器是并行调用的,因此需要组合器来对各个累加值求和。

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

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

class Person {
    String name;
    int age;

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

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

List 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 filtered =
    persons
        .stream()
        .filter(p -> p.name.startsWith("P"))
        .collect(Collectors.toList());

System.out.println(filtered);

代码输出:

 [Peter, Pamela]

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

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

Map> personsByAge = persons
    .stream()
    .collect(Collectors.groupingBy(p -> p.age));

personsByAge
    .forEach((age, p) -> System.out.format("age %s: %s
", 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 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 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 bars = new ArrayList<>();

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

class Bar {
    String name;

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

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

List 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
", sum, p);
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s
", 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
", sum, p);
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s
", 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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72913.html

相关文章

  • Java 8 Strem基本操作

    摘要:可以使用方法替换常规循环以上代码的产出所有这些原始流都像常规对象流一样工作,但有以下不同之处原始流使用专门的表达式,例如代替或代替。原始流支持额外的终端聚合操作,以上代码的产出有时将常规对象流转换为基本流是有用的,反之亦然。 本文提供了有关Java 8 Stream的深入概述。当我第一次读到的Stream API,我感到很困惑,因为它听起来类似Java I/O的InputStream,...

    Jensen 评论0 收藏0
  • Java8-10-Stream分组与分区详解

    摘要:上一篇我们介绍了的概念与实际的一些操作,本篇我们继续来学习的另一个重要操作,分组与分区。注意到分组后的返回类型是,结果集中会将作为,对应的集合作为返回。 上一篇我们介绍了Strem的概念与实际的一些操作,本篇我们继续来学习Stream的另一个重要操作,分组与分区。我们在上一篇介绍Stream的操作时,会经常使用到Collectors这个类,这个类实际上是一个封装了很多常用的汇聚操作的一...

    shengguo 评论0 收藏0
  • cnn卷积神经网络打造人脸登录系统

    摘要:本文基于环境,采用为基础来构建实时人脸检测与识别系统,探索人脸识别系统在现实应用中的难点。对于人脸检测方法,效果好于的方法,但是检测力度也难以达到现场应用标准。本文中,我们采用了基于深度学习方法的人脸检测系统。 git地址:https://github.com/chenlinzho... 本文主要介绍了系统涉及的人脸检测与识别的详细方法,该系统基于python2.7.10/opencv...

    jackwang 评论0 收藏0
  • cnn卷积神经网络打造人脸登录系统

    摘要:本文基于环境,采用为基础来构建实时人脸检测与识别系统,探索人脸识别系统在现实应用中的难点。对于人脸检测方法,效果好于的方法,但是检测力度也难以达到现场应用标准。本文中,我们采用了基于深度学习方法的人脸检测系统。 git地址:https://github.com/chenlinzho... 本文主要介绍了系统涉及的人脸检测与识别的详细方法,该系统基于python2.7.10/opencv...

    KavenFan 评论0 收藏0
  • Java 8 函数式编程」读书笔记——高级集合类和收集器

    摘要:本章是该书的第五章主要讲了方法引用和收集器方法引用形如这样的表达式可以简写为这种简写的语法被称为方法引用方法引用无需考虑参数因为一个方法引用可以在不同的情况下解析为不同的表达式这依赖于的推断方法引用的类型方法引用可以分为四类引用静态方法 本章是该书的第五章, 主要讲了方法引用和收集器 方法引用 形如: artist -> artist.getName() (String arg) ->...

    imingyu 评论0 收藏0

发表评论

0条评论

dadong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<