Java 8 Strem高階操作

qianmoQ發表於2019-01-19

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
", 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
", 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

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

相關文章