【Java8新特性】——Stream的reduce及Collect使用方式
技術標籤:# 【Java新特性】【Java】java8 reduce
文章目錄
前言
本文主要講解關於Stream中reduce的使用方式以及Collect使用方式,同時展示如何自定義收集器。
提示:如果大家對lambda表示式中的四大基礎函式不清楚,推薦大家優先看下四大內建核心函式式介面以及看下關於reduce相關api的使用,
一、Reduce
Reduce中文含義為:減少、縮小;而Stream中的Reduce方法乾的正是這樣的活:根據一定的規則將Stream中的元素進行計算後返回一個唯一的值。
它有三個變種,輸入引數分別是一個引數、二個引數以及三個引數;
1.1一個引數的Reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
先解釋基本概念
BiFunction
R apply(T t, U u);
函式式介面與Function不同點在於它接收兩個輸入返回一個輸出;Function接收一個輸入返回一個輸出。兩個輸入,一個輸出的型別可以不同。
BinaryOperator
public interface BinaryOperator<T> extends BiFunction<T,T,T>
繼承BiFunction,相比BinaryOperator直接限定其三個引數必須一樣,表示兩個相同型別的輸入經過計算後產生一個同類型的輸出。
BinaryOperator介面,可以看到reduce方法接受一個函式,這個函式有兩個引數,第一個引數是上次函式執行的返回值(也稱為中間結果),第二個引數是stream中的元素,函式將兩個值按照方法處理,得到值賦給下次執行這個函式的引數。第一次執行的時候第一引數的值是stream的第一元素,第二個元素是stream的第二元素
Optional accResult = Stream.of(1, 2, 3)
.reduce((acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult.get());
System.out.println("--------");
// 結果列印
--------
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
accResult: 6
--------
1.2二個引數的Reduce
T reduce(T identity, BinaryOperator<T> accumulator);
與第一個變形不同的,會接受一個返回值型別相同identity引數,用於指定Stream的迴圈初始值,如果Stream為空,則預設返回identity,則不會再返回null值。
int accResult = Stream.of(1, 2, 3)
.reduce(0, (acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult);
System.out.println("--------");
// 結果列印
acc : 0
item: 1
acc+ : 1
--------
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
accResult: 6
--------
1.3三個引數的Reduce
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
解釋過前兩個值的概念,第三個值combiner作用,主要用於併發進行的 為了避免競爭 每個reduce執行緒都會有獨立的result combiner的作用在於合併每個執行緒的result得到最終結果。如果Stream是非並行時,第三個引數實際上是不生效的。
但是如果Stream是並行,第三個引數有了意義,將不同執行緒計算的結果呼叫combiner做彙總後返回。
System.out.println("----------stream---------");
System.out.println(Stream.of(1, 2, 3).reduce(4, (s1, s2) -> s1 + s2
, (s1, s2) -> s1 + s2));
System.out.println("----------parallel stream---------");
System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, (s1, s2) -> s1 + s2
, (s1, s2) -> s1 + s2));
----------stream---------
10
----------parallel stream---------
18
此時看下非並行和並行的列印結果,運算結果並不一樣,原因在哪?
非並行
計算過程:第一步計算4 + 1 = 5,第二步是5 + 2 = 7,第三步是7 + 3 = 10。按非並行的方式來看它是分了三步的,每一步都要依賴前一步的運算結果。
並行
計算過程:平行計算時,執行緒之間沒有影響,因此每個執行緒在呼叫第二個引數BiFunction進行計算時,直接都是使用result值當其第一個引數(由於Stream計算的惰性處理,在呼叫最終方法前,都不會進行實際的運算,因此每個執行緒取到的result值都是原始的4),因此計算過程現在是這樣的:執行緒1:1 + 4 = 5;執行緒2:2 + 4 = 6;執行緒3:3 + 4 = 7;Combiner函式: 5 + 6 + 7 = 18
二、Collect
collect就是收集器,是Stream一種通用的、從流生成複雜值的結構。只要將它傳給collect方法,也就是所謂的轉換方法,其就會生成想要的資料結構。
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
- supplier:動態的提供初始化的值;建立一個可變的結果容器(JAVADOC);對於平行計算,這個方法可能被呼叫多次,每次返回一個新的物件;
- accumulator:型別為BiConsumer,注意這個介面是沒有返回值的;它必須將一個元素放入結果容器中(JAVADOC)。
- combiner:型別也是BiConsumer,因此也沒有返回值。它與三引數的Reduce型別,只是在平行計算時彙總不同執行緒計算的結果。它的輸入是兩個結果容器,必須將第二個結果容器中的值全部放入第一個結果容器中(JAVADOC)。
BiConsumer
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
可見它就是一個兩個輸入引數的Consumer的變種。計算沒有返回值。即消費型。
與reduce一樣,同樣分為並行和非並行,下面講解下並行
/**
* 模擬Filter查詢其中含有字母a的所有元素,列印結果將是aa ab ad
*/
Stream<String> s1 = Stream.of("aa", "ab", "c", "ad");
Predicate<String> predicate = t -> t.contains("a");
System.out.println(s1.parallel().collect(() -> new ArrayList<String>(),
(array, s) -> {if (predicate.test(s)) array.add(s); },
(array1, array2) -> array1.addAll(array2)));
每個執行緒都建立了一個結果容器ArrayList,假設每個執行緒處理一個元素,那麼處理的結果將會是[aa],[ab],[],[ad]四個結果容器(ArrayList);最終再呼叫第三個BiConsumer引數將結果全部Put到第一個List中,因此返回結果就是列印的結果了。
三、Collector
Collector是Stream的可變減少操作介面,可變減少操作包括:將元素累積到集合中,使用StringBuilder連線字串;計算元素相關的統計資訊,例如sum,min,max或average等。Collectors這個工具庫,在該庫中封裝了相應的轉換方法。當然,Collectors工具庫僅僅封裝了常用的一些情景,如果有特殊需求,那就要自定義了。
Collector<T, A, R>接受三個泛型引數,對可變減少操作的資料型別作相應限制:
T:輸入元素型別
A:縮減操作的可變累積型別(通常隱藏為實現細節)
R:可變減少操作的結果型別
Collector介面聲明瞭4個函式,這四個函式一起協調執行以將元素目累積到可變結果容器中,並且可以選擇地對結果進行最終的變換.
Supplier<A> supplier(): 建立新的結果結
BiConsumer<A, T> accumulator(): 將元素新增到結果容器
BinaryOperator<A> combiner(): 將兩個結果容器合併為一個結果容器
Function<A, R> finisher(): 對結果容器作相應的變換
Collector介面聲明瞭4個函式,這四個函式一起協調執行以將元素目累積到可變結果容器中,並且可以選擇地對結果進行最終的變換.
在Collector介面的characteristics方法內,可以對Collector宣告相關約束
Set<Characteristics> characteristics():
而Characteristics是Collector內的一個列舉類,聲明瞭CONCURRENT、UNORDERED、IDENTITY_FINISH等三個屬性,用來約束Collector的屬性。
CONCURRENT:表示此收集器支援併發,意味著允許在多個執行緒中,累加器可以呼叫結果容器
UNORDERED:表示收集器並不按照Stream中的元素輸入順序執行
IDENTITY_FINISH:表示finisher實現的是識別功能,可忽略。
注:如果一個容器僅宣告CONCURRENT屬性,而不是UNORDERED屬性,那麼該容器僅僅支援無序的Stream在多執行緒中執行。
四、定製收集器
定製收集器需要實現Collector介面
實現Collector介面需要給定三個泛型
第一個泛型:收集元素的型別
第二個泛型:累加器的型別
第三個泛型:最終結果的型別
static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;
CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}
CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}
@Override
public BiConsumer<A, T> accumulator() {
return accumulator;
}
@Override
public Supplier<A> supplier() {
return supplier;
}
@Override
public BinaryOperator<A> combiner() {
return combiner;
}
@Override
public Function<A, R> finisher() {
return finisher;
}
@Override
public Set<Characteristics> characteristics() {
return characteristics;
}
}
對應著Collector中四個流程來完成
Supplier<A> supplier(): 建立新的結果結
BiConsumer<A, T> accumulator(): 將元素新增到結果容器
BinaryOperator<A> combiner(): 將兩個結果容器合併為一個結果容器
Function<A, R> finisher(): 對結果容器作相應的變換
第一步生成新的結果集Supplier supplier(): 建立新的結果結
public Supplier<StringJoiner> supplier() {
return () -> new StringJoiner(delim, prefix, suffix);
}
第二步結合之前操作的結果和當前值,生成並返回新的值,即BiConsumer<A, T> accumulator()
public BiConsumer<StringJoiner, String> accumulator() {
return StringJoiner::add;
}
第三步將兩個容器合併,由於Collector支援併發操作,如果不將多的容器合併,必然會導致資料的混亂。如果僅僅在序列執行,此步驟可以省略。即BinaryOperator combiner()
public BinaryOperator<StringJoiner> combiner() {
return StringJoiner::merge;
}
第四步處理返回值,即Function<A, R> finisher(): 對結果容器作相應的變換。
public Function<StringJoiner, String> finisher() {
return StringJoiner::toString;
}
Collector自定義起來,也不是特別的麻煩,不過要明確以下幾點:
- 引數型別:這裡最重要的是指定累加器的型別,一般都是自定義的過度類
待收集元素的型別:T;
累加器的型別:A;
最終結果的型別:R。 - 累加器的邏輯
- 最終結果的轉換
- Collector特徵的選擇
具體demo推薦博文java8之定製收集器
總結
主要講reduce三種使用方式,其中方式1,2兩種變形的區別在於返回值是否可以為空,變形二因為在初始化已經賦有預設值,則不存在為空的可能,方式3與前者的區別要注意在並行情況下的使用,在提升效率的同時,注意是否預期效果相符。關於collect本質上就是收集器,通過收集元素,累加器的處理以及最後結果轉換,達到複用的效果,同時提升使用效率。