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