Java 8 Stream 教程 (二)

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

(扫码或长按识别)

Java 8 Stream 教程 (二)