Java函數語言程式設計中歸約reduce()的使用教程
歸約Reduce流運算允許我們透過對序列中的元素重複應用合併操作,從而從元素序列中產生一個單一結果。其中參與者有三者:
- 標識identity:代表一個元素,它是歸約reduce運算的初始值,如果流為空,則為此預設結果。
- Accumulator 累加器:具有兩個引數的函式:歸約reduce運算後的部分結果和流的下一個元素
- Combiner 組合器:當歸約是並行化或累加器引數的型別與累加器實現的型別不匹配時,用於合併combine歸約操作的部分結果的函式
可以轉為reduce的for-loop:
List<String> letters = Arrays.asList("a","bb","ccc"); boolean seen = false; String acc = null; for (String letter : letters) { if (!seen) { seen = true; acc = letter; } else { acc = acc.length() < letter.length() ? acc : letter; } } |
對應的reduce函式:
letters.stream() .reduce((partialString, element) -> { System.out.println(partialString + " " + element); return partialString.length() < element.length() ? partialString : element; }).get(); // output a bb a ccc |
- 這裡partialString是identity標識,它儲存reduce的初始值,也是stream是空的預設結果;
- partialString後面是一個累加器,它會將使用partialString和Stream中一個元素實現運算獲得結果;
當Stream並行執行時,Java執行時會將流拆分為多個子流。在這種情況下,我們需要使用一種函式將子流的結果合併為一個。 這就是組合器的作用。
List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32); int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum); |
上面Integer :: sum方法是組合器。
如果我們使用順序流並且累加器引數的型別和其實現的型別匹配,則無需使用組合器。
使用並行流時,應確保reduce()或在流上執行的任何其他聚合累積操作是:
- associative互換的:結果不受運算元順序的影響
- 無干擾:操作不會影響資料來源
- 無狀態和確定性的:該操作沒有狀態,並且對於給定的輸入產生相同的輸出
- 無干擾:操作不會影響資料來源
我們應該滿足所有這些條件,以防止出現不可預測的結果。
並行流比順序流的效能要好得多,當我們需要使用大型流並執行昂貴的聚合操作時,並行化流是正確的方法。
看看下面是一個字串合併的reduce不正確用法:
public String concatenate(List<Character> chars) { return chars.stream() .reduce(new StringBuilder(""), (acc, c) -> acc.append(c)).toString(); } |
上面reduce兩個引數:identity標識引數、累積器accumulator引數。
注意:這裡用法是不正確的,reduce對累加器有要求的,應該是可交換、無干擾和無狀態的,由於這裡identity標識new StringBuilder("")是可變的,這個初始值因為追加新的字元變化,因此在並行執行的情況下,結果將被破壞。
修復:
public static String concatenate(List<Character> chars) { return chars .stream() .reduce(new StringBuilder(), StringBuilder::append, StringBuilder::append).toString(); } |
它類似下面程式碼:
U result = identity; for (T element : this stream) result = accumulator.apply(result, element) return result; |
上面做法效能不好!這樣的實現將進行大量的字串複製,並且執行時間的字元數將為O(n ^ 2)。一種更有效的方法是將結果累積到中StringBuilder,這是用於累積字串的可變容器。我們可以像使用普通歸約法一樣使用相同的技術來並行化可變歸約法。
可變歸約運算稱為 collect(),因為它將所需的結果收集到結果容器中Collection。
reduce與collect
與reduce方法(該方法在處理元素時總是會建立一個新值)不同,collect方法會修改或變異現有值。一個collect操作需要三個函式引數:
- supplier供應商函式,用於構造結果容器的新例項;
- 累加器功能,用於將輸入元素合併到結果容器中;
- 以及組合combiner功能,用於將一個結果容器中的內容合併到另一個結果容器中。
- 累加器功能,用於將輸入元素合併到結果容器中;
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); |
與reduce()一樣,collect以這種抽象方式表示的好處是它直接適合於並行化:只要累加和組合函式滿足適當的要求,我們就可以並行累加部分結果,然後將它們組合起來。例如,要將流中元素的String表示形式收集到中ArrayList,我們可以編寫明顯的順序for-each形式:
ArrayList<String> strings = new ArrayList<>(); for (T element : stream) { strings.add(element.toString()); } |
或者我們可以使用可並行化的收集形式:
ArrayList<String> strings = stream.collect(() -> new ArrayList<>(), (c, e) -> c.add(e.toString()), (c1, c2) -> c1.addAll(c2)); |
或者,將map操作從累加器函式中拉出,我們可以更簡潔地表示為:
List<String> strings = stream.map(Object::toString) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); |
我們的供應商就是ArrayList constructor,累加器將字串化的元素新增到ArrayList中 ,而addAll 合併器只是用來將字串從一個容器複製到另一個容器中。
這個程式碼實際是一個Map/reduce模式。
reduce vs. fold
這兩個函式之間的區別在於,fold()取一個初始值並將其用作第一步的累加值,而reduce()的第一步則將第一和第二個元素用作第一步的操作引數。kotlin程式碼:
val numbers = listOf(5, 2, 10, 4) val sum = numbers.reduce { sum, element -> sum + element } println(sum) val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 } println(sumDoubled) //val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 } //incorrect: the first element isn't doubled in the result //println(sumDoubledReduce) |
fold()用於計算加倍元素的總和。如果將相同的函式傳遞給reduce(),則它將返回另一個結果,因為它在第一步使用列表的第一個和第二個元素作為引數,因此第一個元素不會被加倍。
java中collect類似kotlin的fold。
網友啟發觀點:
reduce實際是累計sum或Accumulator!
是不是類似SQL中5個巢狀的JOINS和GROUP BY?
它實際是一直尾遞tail-recursive功能!
相關文章
- Java中的函數語言程式設計(七)流Stream的Map-Reduce操作Java函數程式設計
- Python函數語言程式設計-map/reducePython函數程式設計
- Python中的Map、Reduce和Filter函數語言程式設計PythonFilter函數程式設計
- 在JavaScript函數語言程式設計裡使用Map和Reduce方法JavaScript函數程式設計
- Java 函數語言程式設計Java函數程式設計
- 函數語言程式設計入門教程函數程式設計
- JavaScript中的函數語言程式設計JavaScript函數程式設計
- JavaScript 中的函數語言程式設計JavaScript函數程式設計
- Java 中的資料流和函數語言程式設計Java函數程式設計
- Java中的函數語言程式設計(三)lambda表示式Java函數程式設計
- Java 函數語言程式設計的前生今世Java函數程式設計
- Java8的函數語言程式設計Java函數程式設計
- 《Java 8函數語言程式設計》選讀:為什麼要給Java 8中加入函數語言程式設計?Java函數程式設計
- Java中的函數語言程式設計(八)流Stream並行程式設計Java函數程式設計並行行程
- 使用 Java 8 函數語言程式設計生成字母序列Java函數程式設計
- Js中函數語言程式設計的理解JS函數程式設計
- Python 中的函數語言程式設計Python函數程式設計
- 函數語言程式設計中的常用技巧函數程式設計
- 函數語言程式設計函數程式設計
- Java中的函數語言程式設計(二)函式式介面Functional InterfaceJava函數程式設計函式Function
- 淺談Java 8的函數語言程式設計Java函數程式設計
- Java中函數語言程式設計Monad概念介紹Java函數程式設計
- Scala 函數語言程式設計(一) 什麼是函數語言程式設計?函數程式設計
- Java 函數語言程式設計(三)流(Stream)Java函數程式設計
- Java函數語言程式設計知識分享!Java函數程式設計
- Python函數語言程式設計入門教程Python函數程式設計
- 【譯】JavaScript 中的函數語言程式設計原理JavaScript函數程式設計
- 使用 Go 泛型的函數語言程式設計Go泛型函數程式設計
- RAC的函數語言程式設計函數程式設計
- 隨便聊聊 Java 8 的函數語言程式設計Java函數程式設計
- [譯]通往 Java 函數語言程式設計的捷徑Java函數程式設計
- 函數語言程式設計,真香函數程式設計
- javascript函數語言程式設計JavaScript函數程式設計
- 初探函數語言程式設計函數程式設計
- 函數語言程式設計初探函數程式設計
- JavaScript 函數語言程式設計JavaScript函數程式設計
- 一個簡單的JavaScript函數語言程式設計教程JavaScript函數程式設計
- 重識Java8函數語言程式設計Java函數程式設計