Java 8 Strem高級操做

Streams支持大量不一樣的操做。咱們已經瞭解了最重要的操做,如filtermap。發現全部其餘可用的操做(參見Stream Javadoc)。咱們深刻研究更復雜的操做collectflatMapreducehtml

本節中的大多數代碼示例使用如下人員列表進行演示:java

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類支持各類內置收集器。所以,對於最多見的操做,您沒必要本身實現收集器。api

讓咱們從一個很是常見的用例開始:oracle

List<Person> filtered =
    persons
        .stream()
        .filter(p -> p.name.startsWith("P"))
        .collect(Collectors.toList());

System.out.println(filtered);
複製代碼

代碼輸出:ide

[Peter, Pamela]
複製代碼

正如您所看到的,流的元素構造列表很是簡單。須要一個集合而不是列表 - 只需使用Collectors.toList()函數

下一個示例按年齡對全部人進行分組:this

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));
複製代碼

代碼產出spa

age 18: [Max]
age 23: [Peter, Pamela]
age 12: [David]
複製代碼

您還能夠在流的元素上建立聚合,例如,肯定全部人的平均年齡:調試

Double averageAge = persons
    .stream()
    .collect(Collectors.averagingInt(p -> p.age));

System.out.println(averageAge); 
複製代碼

代碼產出code

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
複製代碼

並行執行此流會致使徹底不一樣的執行行爲。如今實際上調用了組合器。因爲累加器是並行調用的,所以須要組合器來對各個累加值求和。

相關文章
相關標籤/搜索