1. 程式人生 > >JAVA8-LAMBDA中reduce的用法

JAVA8-LAMBDA中reduce的用法

reduce 操作可以實現從Stream中生成一個值,其生成的值不是隨意的,而是根據指定的計算模型。比如,之前提到count、min和max方 
法,因為常用而被納入標準庫中。事實上,這些方法都是reduce操作。

reduce方法有三個override的方法:

  • Optional<T> reduce(BinaryOperator<T> accumulator);
  • T reduce(T identity, BinaryOperator<T> accumulator);
  • <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

我們先看第一個變形,其接受一個函式介面BinaryOperator<T>,而這個介面又繼承於BiFunction<T, T, T>.在BinaryOperator介面中,又定義了兩個靜態方法minBy和maxBy。這裡我們先不管這兩個靜態方法,先了解reduce的操作。

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }


    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

在使用時,我們可以使用Lambada表示式來表示

BinaryOperator介面,可以看到reduce方法接受一個函式,這個函式有兩個引數,第一個引數是上次函式執行的返回值(也稱為中間結果),第二個引數是stream中的元素,這個函式把這兩個值相加,得到的和會被賦值給下次執行這個函式的第一個引數。要注意的是:第一次執行的時候第一個引數的值是Stream的第一個元素,第二個引數是Stream的第二個元素。這個方法返回值型別是Optional,

Optional accResult = Stream.of(1, 2, 3, 4)
        .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
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
--------

這裡寫圖片描述

下面來看第二個變形,與第一種變形相同的是都會接受一個BinaryOperator函式介面,不同的是其會接受一個identity引數,用來指定Stream迴圈的初始值。如果Stream為空,就直接返回該值。另一方面,該方法不會返回Optional,因為該方法不會出現null。

int accResult = Stream.of(1, 2, 3, 4)
            .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
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
--------

從列印結果可以看出,reduce前兩種變形,因為接受引數不同,其執行的操作也有相應變化:

  • 變形1,未定義初始值,從而第一次執行的時候第一個引數的值是Stream的第一個元素,第二個引數是Stream的第二個元素
  • 變形2,定義了初始值,從而第一次執行的時候第一個引數的值是初始值,第二個引數是Stream的第一個元素

對於第三種變形,我們先看各個引數的含義,第一個引數返回例項u,傳遞你要返回的U型別物件的初始化例項u,第二個引數累加器accumulator,可以使用二元?表示式(即二元lambda表示式),宣告你在u上累加你的資料來源t的邏輯,例如(u,t)->u.sum(t),此時lambda表示式的行參列表是返回例項u和遍歷的集合元素t,函式體是在u上累加t,第三個引數組合器combiner,同樣是二元?表示式,(u,t)->u。在官方文件上有這麼一段介紹,

U result = identity; 

for (T element : this stream) 

        result = accumulator.apply(result, element) 

return result;

but is not constrained to execute sequentially.

The identity value must be an identity for the combiner function. This means that for all u, combiner(identity, u) is equal to u. Additionally, the combiner function must be compatible with the accumulator function; for all u and t, the following must hold:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

因為reduce的變形的第一個引數型別是實際返回例項的資料型別,同時其為一個泛型也就是意味著該變形的可以返回任意型別的資料。從上面文件介紹的字面意思解讀是第三個引數函式用來組合兩個值,而這兩個值必須與第二個函式引數相相容,也就是說它們所得的結果是一樣的。看到這裡肯定有迷惑的地方,第三個引數到底是用來幹嘛的?我們先看一段程式碼,為了便於瞭解其中的緣由,並沒有使用Lambda表示式,

ArrayList<Integer> accResult_ = Stream.of(1, 2, 3, 4)
        .reduce(new ArrayList<Integer>(),
                new BiFunction<ArrayList<Integer>, Integer, ArrayList<Integer>>() {
                    @Override
                    public ArrayList<Integer> apply(ArrayList<Integer> acc, Integer item) {

                        acc.add(item);
                        System.out.println("item: " + item);
                        System.out.println("acc+ : " + acc);
                        System.out.println("BiFunction");
                        return acc;
                    }
                }, new BinaryOperator<ArrayList<Integer>>() {
                    @Override
                    public ArrayList<Integer> apply(ArrayList<Integer> acc, ArrayList<Integer> item) {
                        System.out.println("BinaryOperator");
                        acc.addAll(item);
                        System.out.println("item: " + item);
                        System.out.println("acc+ : " + acc);
                        System.out.println("--------");
                        return acc;
                    }
                });
System.out.println("accResult_: " + accResult_);
// 結果列印
item: 1
acc+ : [1]
BiFunction
item: 2
acc+ : [1, 2]
BiFunction
item: 3
acc+ : [1, 2, 3]
BiFunction
item: 4
acc+ : [1, 2, 3, 4]
BiFunction
accResult_: [1, 2, 3, 4]
accResult_: 10

首先示例程式碼中,傳遞給第一個引數是ArrayList,在第二個函式引數中列印了“BiFunction”,而在第三個引數介面中列印了函式介面中列印了”BinaryOperator“.可是,看列印結果,只是列印了“BiFunction”,而沒有列印”BinaryOperator“,說明第三個函式引數並沒有執行。這裡我們知道了該變形可以返回任意型別的資料。對於第三個函式引數,為什麼沒有執行,剛開始的時候也是沒有看懂到底是啥意思呢,而且其引數必須為返回的資料型別?看了好幾遍文件也是一頭霧水。在 java8 reduce方法中的第三個引數combiner有什麼作用?這裡找到了答案,Stream是支援併發操作的,為了避免競爭,對於reduce執行緒都會有獨立的result,combiner的作用在於合併每個執行緒的result得到最終結果。這也說明了了第三個函式引數的資料型別必須為返回資料型別了。


需要注意的是,因為第三個引數用來處理併發操作,如何處理資料的重複性,應多做考慮,否則會出現重複資料