Java 8 Strem高級操作

語言: CN / TW / HK

Streams支持大量不同的操作。我們已經瞭解了最重要的操作,如filtermap。發現所有其他可用的操作(參見Stream Javadoc)。我們深入研究更復雜的操作collectflatMapreduce

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

```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 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類支持各種內置收集器。因此,對於最常見的操作,您不必自己實現收集器。

讓我們從一個非常常見的用例開始:

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

System.out.println(filtered); ```

代碼輸出:

java [Peter, Pamela]

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

下一個示例按年齡對所有人進行分組:

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

personsByAge .forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); ```

代碼產出

java age 18: [Max] age 23: [Peter, Pamela] age 12: [David]

您還可以在流的元素上創建聚合,例如,確定所有人的平均年齡:

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

System.out.println(averageAge); ```

代碼產出

java 19.0

如果您對更全面的統計信息感興趣,彙總收集器將返回一個特殊的內置摘要統計信息對象。因此,我們可以簡單地確定人的最小,最大和算術平均年齡以及總和和計數。

```java IntSummaryStatistics ageSummary = persons .stream() .collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary); ```

代碼產出

java IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

下一個示例將所有人連接成一個字符串:

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

代碼產出

java In Germany Max and Peter and Pamela are of legal age.

Collect接受分隔符以及可選的前綴和後綴。

為了將流元素轉換為映射,我們必須指定如何映射鍵和值。請記住,映射的鍵必須是唯一的,否則拋出一個IllegalStateException。您可以選擇將合併函數作為附加參數傳遞以繞過異常:

```java Map map = persons .stream() .collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2));

System.out.println(map); ```

代碼產出

java {18=Max, 23=Peter;Pamela, 12=David}

現在我們知道了一些強大的Collect,讓我們嘗試構建我們自己的特殊Collect。我們希望將流的所有人轉換為單個字符串,該字符串由|管道字符分隔的大寫字母組成。為了實現這一目標,我們創建了一個新的Collector.of()

```java 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實際操作之前,我們需要一個適當的類型層

```java class Foo { String name; List bars = new ArrayList<>();

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

}

class Bar { String name;

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

} ```

接下來,我們利用有關流的知識來實例化幾個對象:

```java 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對象,我們只傳遞相應的函數:

java foos.stream() .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name));

代碼產出

java Bar1 <- Foo1 Bar2 <- Foo1 Bar3 <- Foo1 Bar1 <- Foo2 Bar2 <- Foo2 Bar3 <- Foo2 Bar1 <- Foo3 Bar2 <- Foo3 Bar3 <- Foo3

如您所見,我們已成功將三個foo對象的流轉換為九個bar對象的流。

最後,上面的代碼示例可以簡化為流操作的單個管道:

java 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檢查。

這樣一個高度分層的結構:

```java class Outer { Nested nested; }

class Nested { Inner inner; }

class Inner { String foo; } ```

為了解析foo外部實例的內部字符串,您必須添加多個空值檢查以防止可能的NullPointerExceptions:

java Outer outer = new Outer(); if (outer != null && outer.nested != null && outer.nested.inner != null) { System.out.println(outer.nested.inner.foo); }

利用選項flatMap操作可以獲得相同的行為:

java 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方法。第一個將元素流簡化為流的一個元素。讓我們看看我們如何使用這種方法來確定最老的人:

java persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println); // Pamela

reduce方法接受一個BinaryOperator累加器函數。這實際上是一個雙函數,兩個操作數共享同一類型,在這種情況下是Person。雙函數類似於函數,但接受兩個參數。示例函數比較兩個人的年齡,以返回年齡最大的人。

第二種reduce方法接受標識值和BinaryOperator累加器。此方法可用於構造一個新的Person,其中包含來自流中所有其他人的聚合名稱和年齡:

```java 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來確定所有人的年齡總和:

```java Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);

System.out.println(ageSum); // 76 ```

正如你所看到的結果是76,但是究竟發生了什麼?讓我們通過一些調試輸出擴展上面的代碼:

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

代碼產出

java accumulator: sum=0; person=Max accumulator: sum=18; person=Peter accumulator: sum=41; person=Pamela accumulator: sum=64; person=David

正如你所看到的,累加器函數完成了所有的工作。它首先以初始恆等值0和第一個person Max被調用。在接下來的三個步驟中,總和隨着最後一個步驟的年齡不斷增加,人的總年齡達到76歲。

為什麼組合器永遠不會被調用?並行執行相同的流將解除祕密​​:

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

代碼產出

java 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

並行執行此流會導致完全不同的執行行為。現在實際上調用了組合器。由於累加器是並行調用的,因此需要組合器來對各個累加值求和。