JAVA8-stream學習

weixin_34166847發表於2018-04-07

stream

執行順序

資料流操作要麼是銜接操作,要麼是終止操作。
銜接操作返回資料流,所以我們可以把多個銜接操作不使用分號來連結到一起。 終止操作無返回值,或者返回一個不是流的結果。在上面的例子中,filter、map和sorted都是銜接操作,而forEach是終止操作。

銜接操作延遲性

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
複製程式碼

執行這段程式碼時,不向控制檯列印任何東西。這是因為銜接操作只在終止操作呼叫時被執行。

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
複製程式碼

上述程式碼執行輸出結果為:

filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c
複製程式碼

方法會在資料流的所有元素上,一個接一個地水平執行所有操作。但是每個元素在呼叫鏈上垂直移動。第一個字串"d2"首先經過filter然後是forEach,執行完後才開始處理第二個字串"a2"。

Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});

// map:      d2
// anyMatch: D2
// map:      a2
// anyMatch: A2
複製程式碼

只要提供的資料元素滿足了謂詞,anyMatch操作就會返回true。對於第二個傳遞"A2"的元素,它的結果為真。由於資料流的鏈式呼叫是垂直執行的,map這裡只需要執行兩次。所以map會執行儘可能少的次數,而不是把所有元素都對映一遍。

Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));

// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C
複製程式碼

就像你可能猜到的那樣,map和filter會對底層集合的每個字串呼叫五次,而forEach只會呼叫一次。 如果我們調整操作順序,將filter移動到呼叫鏈的頂端,就可以極大減少操作的執行次數:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));

// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c
複製程式碼

sorted

Stream.of("d2", "a2", "b1", "b3", "c")
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2);
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));
    sort:    a2; d2
sort:    b1; a2
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2
複製程式碼

sorted是水平執行的且是有狀態的。

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2);
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));

// filter:  d2
// filter:  a2
// filter:  b1
// filter:  b3
// filter:  c
// map:     a2
// forEach: A2
複製程式碼

上例中sorted沒有執行,因為filter把資料減少到只有一條。

複用資料流

Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
複製程式碼

要克服這個限制,我們需要為每個我們想要執行的終止操作建立新的資料流呼叫鏈。例如,我們建立一個資料流供應器,來構建新的資料流,並且設定好所有銜接操作:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok 
複製程式碼

高階操作

據流執行大量的不同操作。我們已經瞭解了一些最重要的操作,例如filter和map。我將它們留給你來探索所有其他的可用操作。下面讓我們深入瞭解一些更復雜的操作:collect、flatMap和reduce。 Person類

package cn.duming.stream;

import java.util.Arrays;
import java.util.List;

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name;
    }
    public static void main(String [] args){
        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),它由四個不同的操作組成:供應器(supplier)、累加器(accumulator)、組合器(combiner)和終止器(finisher)。這在開始聽起來十分複雜,但是Java8通過內建的Collectors類支援多種內建的收集器。所以對於大部分常見操作,你並不需要自己實現收集器。

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

System.out.println(filtered); // [Peter, Pamela]
複製程式碼

就像你看到的那樣,它非常簡單,只是從流的元素中構造了一個列表。如果需要以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));

Connected to the target VM, address: '127.0.0.1:54423', transport: 'socket'
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);
複製程式碼

flatMap

我們已經瞭解瞭如何通過使用map操作,將流中的物件轉換為另一種型別。map有時十分受限,因為每個物件只能對映為一個其它物件。但如何我希望將一個物件轉換為多個或零個其他物件呢?flatMap這時就會派上用場。

flatMap將流中的每個元素,轉換為其它物件的流。所以每個物件會被轉換為零個、一個或多個其它物件,以流的形式返回。這些流的內容之後會放進flatMap所返回的流中。

在我們瞭解flatMap如何使用之前,我們需要相應的型別體系:

package cn.duming.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public class FlatMapDemo {

    public static void main(String [] args){
        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))));

    System.out.println(foos);
    }
}
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;
    }
}
複製程式碼

現在我們擁有了含有三個foo的列表,每個都含有三個bar。

flatMap接受返回物件流的函式。所以為了處理每個foo上的bar物件,我們需要傳遞相應的函式:

     foos.stream()
                .flatMap(f -> f.bars.stream())
                .forEach(b -> System.out.println(b.name));
複製程式碼

上訴程式碼整體可以使用如下流水線代替

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

reduce

歸約操作將所有流中的元素組合為單一結果。Java8支援三種不同型別的reduce方法。第一種將流中的元素歸約為流中的一個元素。讓我們看看我們如何使用這個方法來計算出最老的人:

        persons.stream()
                .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
                .ifPresent(System.out::println); // Pamela
複製程式碼

reduce方法接受BinaryOperator積累函式。它實際上是兩個運算元型別相同的BiFunction。BiFunction就像是Function,但是接受兩個引數。示例中的函式比較兩個人的年齡,來返回年齡較大的人。 第二個reduce方法接受一個初始值,和一個BinaryOperator累加器。這個方法可以用於從流中的其它Person物件中構造帶有聚合後名稱和年齡的新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,我們可以使用這個歸約函式來計算所有人的年齡總和。:

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

System.out.println(ageSum); // 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
複製程式碼

參考

知乎java8專欄 java8 stream

相關文章