java8 Reduce

Reduce介紹

reduce 操作可以實現(xiàn)從Stream中生成一個值,其生成的值不是隨意的,而是根據(jù)指定的計算模型。比如,之前提到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);

我們先看第一個變形,其接受一個函數(shù)接口BinaryOperator<T>,而這個接口又繼承于BiFunction<T, T, T>.在BinaryOperator接口中,又定義了兩個靜態(tài)方法minBy和maxBy。這里我們先不管這兩個靜態(tài)方法,先了解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方法接受一個函數(shù),這個函數(shù)有兩個參數(shù),第一個參數(shù)是上次函數(shù)執(zhí)行的返回值(也稱為中間結(jié)果),第二個參數(shù)是stream中的元素,這個函數(shù)把這兩個值相加,得到的和會被賦值給下次執(zhí)行這個函數(shù)的第一個參數(shù)。要注意的是:第一次執(zhí)行的時候第一個參數(shù)的值是Stream的第一個元素,第二個參數(shù)是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("--------");

// 結(jié)果打印

acc : 1
item: 2

acc+ : 3
--------

acc : 3
item: 3

acc+ : 6
--------

acc : 6
item: 4

acc+ : 10
--------

accResult: 10
--------
這里寫圖片描述

下面來看第二個變形,與第一種變形相同的是都會接受一個BinaryOperator函數(shù)接口,不同的是其會接受一個identity參數(shù),用來指定Stream循環(huán)的初始值。如果Stream為空,就直接返回該值。另一方面,該方法不會返回Optional,因為該方法不會出現(xiàn)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("--------");
// 結(jié)果打印
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
--------

從打印結(jié)果可以看出,reduce前兩種變形,因為接受參數(shù)不同,其執(zhí)行的操作也有相應變化:

變形1,未定義初始值,從而第一次執(zhí)行的時候第一個參數(shù)的值是Stream的第一個元素,第二個參數(shù)是Stream的第二個元素
變形2,定義了初始值,從而第一次執(zhí)行時候第一個參數(shù)的值是初始值,第二個參數(shù)是Stream的第一個元素

對于第三種變形,我們先看各個參數(shù)的含義,第一個參數(shù)返回實例u,傳遞你要返回的U類型對象的初始化實例u,第二個參數(shù)累加器accumulator,可以使用二元表達式(即二元lambda表達式),聲明你在u上累加你的數(shù)據(jù)來源t的邏輯,例如(u,t)->u.sum(t),此時lambda表達式的行參列表是返回實例u和遍歷的集合元素t,函數(shù)體是在u上累加t,第三個參數(shù)組合器combiner,同樣是二元表達式,(u,t)->u。

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_);
// 結(jié)果打印
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

首先示例代碼中,傳遞給第一個參數(shù)是ArrayList,在第二個函數(shù)參數(shù)中打印了“BiFunction”,而在第三個參數(shù)接口中打印了函數(shù)接口中打印了”BinaryOperator“.可是,看打印結(jié)果,只是打印了“BiFunction”,而沒有打印”BinaryOperator“,說明第三個函數(shù)參數(shù)病沒有執(zhí)行。這里我們知道了該變形可以返回任意類型的數(shù)據(jù)。對于第三個函數(shù)參數(shù),為什么沒有執(zhí)行,剛開始的時候也是沒有看懂到底是啥意思呢,而且其參數(shù)必須為返回的數(shù)據(jù)類型?看了好幾遍文檔也是一頭霧水。在 java8 reduce方法中的第三個參數(shù)combiner有什么作用?這里找到了答案,Stream是支持并發(fā)操作的,為了避免競爭,對于reduce線程都會有獨立的result,combiner的作用在于合并每個線程的result得到最終結(jié)果。這也說明了了第三個函數(shù)參數(shù)的數(shù)據(jù)類型必須為返回數(shù)據(jù)類型了。

需要注意的是,因為第三個參數(shù)用來處理并發(fā)操作,如何處理數(shù)據(jù)的重復性,應多做考慮,否則會出現(xiàn)重復數(shù)據(jù)!

Collect介紹

在這里插入圖片描述

概述

前面我們使用過collect(toList()),在流中生成列表。實際開發(fā)過程中,List又是我們經(jīng)常用到的數(shù)據(jù)結(jié)構(gòu),但是有時候我們也希望Stream能夠轉(zhuǎn)換生成其他的值,比如Map或者set,甚至希望定制生成想要的數(shù)據(jù)結(jié)構(gòu)。

collect也就是收集器,是Stream一種通用的、從流生成復雜值的結(jié)構(gòu)。只要將它傳給collect方法,也就是所謂的轉(zhuǎn)換方法,其就會生成想要的數(shù)據(jù)結(jié)構(gòu)。這里不得不提下,Collectors這個工具庫,在該庫中封裝了相應的轉(zhuǎn)換方法。當然,Collectors工具庫僅僅封裝了常用的一些情景,如果有特殊需求,那就要自定義了。

顯然,List是能想到的從流中生成的最自然的數(shù)據(jù)結(jié)構(gòu), 但是有時人們還希望從流生成其他值, 比如 Map 或 Set, 或者你希望定制一個類將你想要的東西抽象出來。

前面已經(jīng)講過,僅憑流上方法的簽名,就能判斷出這是否是一個及早求值的操作。 reduce操作就是一個很好的例子, 但有時人們希望能做得更多。
這就是收集器,一種通用的、從流生成復雜值的結(jié)構(gòu)。只要將它傳給collect 方法,所有的流就都可以使用它了。

<R, A> R collect(Collector<? super T, A, R> collector);

<R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner);

轉(zhuǎn)成值

使用collect可以將Stream轉(zhuǎn)換成值。maxBy和minBy允許用戶按照某個特定的順序生成一個值。

averagingDouble:求平均值,Stream的元素類型為double
averagingInt:求平均值,Stream的元素類型為int
averagingLong:求平均值,Stream的元素類型為long
counting:Stream的元素個數(shù)
maxBy:在指定條件下的,Stream的最大元素
minBy:在指定條件下的,Stream的最小元素
reducing: reduce操作
summarizingDouble:統(tǒng)計Stream的數(shù)據(jù)(double)狀態(tài),其中包括count,min,max,sum和平均。
summarizingInt:統(tǒng)計Stream的數(shù)據(jù)(int)狀態(tài),其中包括count,min,max,sum和平均。
summarizingLong:統(tǒng)計Stream的數(shù)據(jù)(long)狀態(tài),其中包括count,min,max,sum和平均。
summingDouble:求和,Stream的元素類型為double
summingInt:求和,Stream的元素類型為int
summingLong:求和,Stream的元素類型為long
示例:

Optional<Integer> collectMaxBy = Stream.of(1, 2, 3, 4)
            .collect(Collectors.maxBy(Comparator.comparingInt(e -> e)));
System.out.println("collectMaxBy:" + collectMaxBy.get());
// 打印結(jié)果
// collectMaxBy:4

分割數(shù)據(jù)塊

collect的一個常用操作將Stream分解成兩個集合。假如一個數(shù)字的Stream,我們可能希望將其分割成兩個集合,一個是偶數(shù)集合,另外一個是奇數(shù)集合。我們首先想到的就是過濾操作,通過兩次過濾操作,很簡單的就完成了我們的需求。

但是這樣操作起來有問題。首先,為了執(zhí)行兩次過濾操作,需要有兩個流。其次,如果過濾操作復雜,每個流上都要執(zhí)行這樣的操作, 代碼也會變得冗余。

這里我們就不得不說Collectors庫中的partitioningBy方法,它接受一個流,并將其分成兩部分:使用Predicate對象,指定條件并判斷一個元素應該屬于哪個部分,并根據(jù)布爾值返回一個Map到列表。因此對于key為true所對應的List中的元素,滿足Predicate對象中指定的條件;同樣,key為false所對應的List中的元素,不滿足Predicate對象中指定的條件

這樣,使用partitioningBy,我們就可以將數(shù)字的Stream分解成奇數(shù)集合和偶數(shù)集合了。

Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
            .collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
// 打印結(jié)果
// collectParti : {false=[1, 3], true=[2, 4]}
這里寫圖片描述

數(shù)據(jù)分組

數(shù)據(jù)分組是一種更自然的分割數(shù)據(jù)操作, 與將數(shù)據(jù)分成true和false兩部分不同,可以使用任意值對數(shù)據(jù)分組。

調(diào)用Stream的collect方法,傳入一個收集器,groupingBy接受一個分類函數(shù),用來對數(shù)據(jù)分組,就像partitioningBy一樣,接受一個Predicate對象將數(shù)據(jù)分成true和false兩部分。我們使用的分類器是一個Function對象,和map操作用到的一樣。

示例:

Map<Boolean, List<Integer>> collectGroup= Stream.of(1, 2, 3, 4)
            .collect(Collectors.groupingBy(it -> it > 3));
System.out.println("collectGroup : " + collectGroup);
// 打印結(jié)果
// collectGroup : {false=[1, 2, 3], true=[4]}

注:

看groupingBy和partitioningBy的例子,他們的效果都是一樣的,都是將Stream的數(shù)據(jù)進行了分割處理并返回一個Map??赡芘e的例子給你帶來了誤區(qū),實際上他們兩個完全是不一樣的。

partitioningBy是根據(jù)指定條件,將Stream分割,返回的Map為Map

這里寫圖片描述

字符串

有時候,我們將Stream的元素(String類型)最后生成一組字符串。比如在Stream.of(“1”, “2”, “3”, “4”)中,將Stream格式化成“1,2,3,4”。

如果不使用Stream,我們可以通過for循環(huán)迭代實現(xiàn)。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

StringBuilder sb = new StringBuilder();

for (Integer it : list) {
    if (sb.length() > 0) {
        sb.append(",");
    }
    sb.append(it);

}
System.out.println(sb.toString());
// 打印結(jié)果
// 1,2,3,4

在Java 1.8中,我們可以使用Stream來實現(xiàn)。這里我們將使用 Collectors.joining 收集Stream中的值,該方法可以方便地將Stream得到一個字符串。joining函數(shù)接受三個參數(shù),分別表示允(用以分隔元素)、前綴和后綴。

示例:

String strJoin = Stream.of("1", "2", "3", "4")
        .collect(Collectors.joining(",", "[", "]"));
System.out.println("strJoin: " + strJoin);
// 打印結(jié)果
// strJoin: [1,2,3,4]

組合Collector

前面,我們已經(jīng)了解到Collector的強大,而且非常的使用。如果將他們組合起來,是不是更厲害呢?看前面舉過的例子,在數(shù)據(jù)分組時,我們是得到的分組后的數(shù)據(jù)列表 collectGroup : {false=[1, 2, 3], true=[4]}。如果我們的要求更高點,我們不需要分組后的列表,只要得到分組后列表的個數(shù)就好了。

這時候,很多人下意識的都會想到,遍歷Map就好了,然后使用list.size(),就可以輕松的得到各個分組的列表個數(shù)。

// 分割數(shù)據(jù)塊
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it % 2 == 0));

Map<Boolean, Integer> mapSize = new HashMap<>();
collectParti.entrySet()
        .forEach(entry -> mapSize.put(entry.getKey(), entry.getValue().size()));

System.out.println("mapSize : " + mapSize);
// 打印結(jié)果
// mapSize : {false=2, true=2}
在partitioningBy方法中,有這么一個變形:

Map<Boolean, Long> partiCount = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it.intValue() % 2 == 0,
                Collectors.counting()));
System.out.println("partiCount: " + partiCount);
// 打印結(jié)果
// partiCount: {false=2, true=2}

在partitioningBy方法中,我們不僅傳遞了條件函數(shù),同時傳入了第二個收集器,用以收集最終結(jié)果的一個子集,這些收集器叫作下游收集器。收集器是生成最終結(jié)果的一劑配方,下游收集器則是生成部分結(jié)果的配方,主收集器中會用到下游收集器。這種組合使用收集器的方式, 使得它們在 Stream 類庫中的作用更加強大。

那些為基本類型特殊定制的函數(shù),如averagingInt、summarizingLong等,事實上和調(diào)用特殊Stream上的方法是等價的,加上它們是為了將它們當作下游收集器來使用的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容