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

代码输出:

 [Peter, Pamela]

正如您所看到的,流的元素结构列表十分简单。需求一个调集而不是列表 – 只需运用Collectors.toList()

下一个示例按年纪对一切人进行分组:

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]

您还能够在流的元素上创立聚合,例如,确认一切人的平均年纪:

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<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}

现在咱们知道了一些强大的Collect,让咱们尝试构建咱们自己的特别Collect。咱们希望将流的一切人转化为单个字符串,该字符串由|管道字符分隔的大写字母组成。为了实现这一方针,咱们创立了一个新的Collector.of()

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,让Collect结构咱们的字符串。供应商最初运用适当的分隔符结构这样的StringJoiner。累加器用于将每个人的大写名称添加到StringJoiner。组合器知道怎么将两个StringJoiners合并为一个。在最终一步中,收拾器从StringJoiner结构所需的String。

FlatMap


咱们现已学会了怎么使用map操作将流的目标转化为另一种类型的目标。Map有点受限,因为每个目标只能映射到另一个目标。可是假如咱们想要将一个目标转化为多个其他目标或许根本不转化它们呢?这是flatMap救援的地方。

FlatMap将流的每个元素转化为其他目标的流。因而,每个目标将被转化为由流支撑的零个,一个或多个其他目标。然后将这些流的内容放入回来flatMap操作流中。

在咱们看到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;
    }
}

接下来,咱们使用有关流的知识来实例化几个目标:

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))));

现在咱们列出了三个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\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

正如你所看到的,累加器函数完成了一切的作业。它首先以初始恒等值0和第一个person Max被调用。在接下来的三个过程中,总和跟着最终一个过程的年纪不断增加,人的总年纪到达76岁。

为什么组合器永久不会被调用?并行履行相同的流将解除秘密​​:

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

并行履行此流会导致彻底不同的履行行为。现在实践上调用了组合器。因为累加器是并行调用的,因而需求组合器来对各个累加值求和。