1. 程式人生 > 其它 >【Java8新特性】——Stream的reduce及Collect使用方式

【Java8新特性】——Stream的reduce及Collect使用方式

技術標籤:# 【Java新特性】【Java】java8 reduce

文章目錄


前言

本文主要講解關於Stream中reduce的使用方式以及Collect使用方式,同時展示如何自定義收集器。


提示:如果大家對lambda表示式中的四大基礎函式不清楚,推薦大家優先看下四大內建核心函式式介面以及看下關於reduce相關api的使用,

Java8 中reduce的基本使用

一、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的第二元素

,因為stream元素集合可能為空,所以這個方法的返回值為Optional。舉個例子感受一下

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自定義起來,也不是特別的麻煩,不過要明確以下幾點:

  1. 引數型別:這裡最重要的是指定累加器的型別,一般都是自定義的過度類
    待收集元素的型別:T;
    累加器的型別:A;
    最終結果的型別:R。
  2. 累加器的邏輯
  3. 最終結果的轉換
  4. Collector特徵的選擇

具體demo推薦博文java8之定製收集器

總結

主要講reduce三種使用方式,其中方式1,2兩種變形的區別在於返回值是否可以為空,變形二因為在初始化已經賦有預設值,則不存在為空的可能,方式3與前者的區別要注意在並行情況下的使用,在提升效率的同時,注意是否預期效果相符。關於collect本質上就是收集器,通過收集元素,累加器的處理以及最後結果轉換,達到複用的效果,同時提升使用效率。