JAVA8你只需要知道這些(3)

前言

在上篇文章中,我們提到了java.util.stream包,今天我們就來詳細的研究一下這個包。

整體框架

分析stream包,我們先從整體架構入手,然後再深入到細節。我們先來看看API文檔:

1.png
1.png

  從上圖中可以看見stream包中的接口比較多,類和枚舉比較少。我們先來看接口:

java8-stream.png
java8-stream.png

  DoubleStream,IntStream,LongStream,Stream都繼承於BaseStream接口。並且它們都有各自的Builder接口:DoubleStream.Builder,IntStream.Builder,LongStream.Builder,Stream.Builder。剩下就只有Collector接口,Collectors,StreamSupport類,Collector,Characteristics枚舉。

Stream接口

Stream接口是一個泛型接口,而DoubleStream,IntStream,LongStream只不過是對double,int,long的包裝而已,所以我們弄懂Stream,其他的接口也都大同小異。

1.forEach

void forEach(Consumer<? super T> action)

forEach接收一個Consumer接口,該接口我們之前講Function包時已經提過了。它只接收不參數,沒有返回值。然後在 Stream 的每一個元素上執行該表達式。

範例:

Stream<String> stream = Stream.of("I", "love", "you");
        stream.forEach(System.out::println);

System.out.println方法我們都很熟悉了,它接收一個參數,並且在控制檯打印出來。這正好符合Consumer接口,所以這裏輸出的結果是 :

I
love
you

2.peek

Stream<T> peek(Consumer<? super T> action)

peek方法也是接收一個Consumer功能型接口,它與forEach的區別就是它會返回Stream接口,也就是說forEach是一個Terminal操作,而peek是一個Intermediate操作,forEach完了以後Stream就消費完了,不能繼續再使用,而peek還可以繼續使用。
範例:

Stream<String> stream = Stream.of("I", "love", "you");
        stream.peek(System.out::println).forEach(System.out::println);

代碼很簡單,但是大家可以先思考一下,輸出的結果是什麼?
輸出結果:

I
I
love
love
you
you

怎麼樣?跟你想的是一樣的嗎?有人可能會問,爲什麼輸出結果不是以下這種呢?

I
love 
you
I
love
you

明明peek方法在前面。這是因爲我們前面提到過的懶加載,peek是一個Intermediate操作,它並不會馬上執行,當forEach的時候纔會把peek和forEach一起執行,來提高效率,所以等於是每個stream元素執行兩次打印操作,再執行下一個元素。

3.filter

Stream<T> filter(Predicate<? super T> predicate)

filter方法接收一個斷言型的接口,斷言型接口接收一個參數,返回一個Boolean類型。filter方法根據某個條件對stream元素進行過濾,通過過濾的元素將生成一個新的stream。
範例:

Stream<Integer> stream = Stream.of(1, 2, 3,4,5,6);
        stream.filter((n)->n>2).forEach(System.out::println);

以上代碼通過filter方法把大於2的元素過濾出來,然後輸出。

4.map

<R> Stream<R> map(Function<? super T,? extends R> mapper)

map方法接收一個功能型接口,功能型接口接收一個參數,返回一個值。map方法的用途是將舊數據轉換後變爲新數據,是一種1:1的映射,每個輸入元素按照規則轉換成另一個元素。該方法是Intermediate操作。

Stream<String> stream = Stream.of("a","b","c","d");
        stream.map(String::toUpperCase).forEach(System.out::println);

以上代碼通過map方法,把a,b,c,d全部轉變成大寫,然後輸出。

5.flatMap

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

flatMap從結構上來看跟map差不多,主要是可以用來將stream層級扁平化。

Stream<List<Integer>> inputStream = Stream.of(
                 Arrays.asList(1),
                 Arrays.asList(2, 3),
                 Arrays.asList(4, 5, 6)
                 );
        inputStream.flatMap((n)->n.stream()).forEach(System.out::println);

我們可以看見,inputStream由3個list組成,在經過flatMap以後,list就沒有了,以前list中的元素全部放在了一起。相關的方法還有:flatMapToInt,flatMapToLong,flatMapToDouble,只不過他們返回的分別是IntStream,LongStrea和DoubleStream。

6.findFirst:返回stream的第一個元素的Optional或爲空。這是一個Terminal操作,也是一個短路操作。

7.count:返回此流元素的數量。

8.sorted:將此流中的元素根據自然順序排序,sorted方法還有一個重載方法,可以傳入一個Comparator,這樣就可以根據Comparator來排序。

9.min/max:Stream接口中的這兩個方法接收一個Comparator參數,通過Comparator返回此流最小或者最大的元素。IntStream,DoubleStream.LongStream則不需要傳入Comparator。

10.limit:該方法接收一個long型參數,表示一共返回幾個元素。

11.skip:接收一個long類型的參數,表示跳過幾個元素。

12.distinct:消除重複元素後返回一個新Stream。

13.allMatch:Stream中的所有元素滿足傳入的斷言型接口,就返回true。

14.anyMatch:Stream中的只要有一個元素滿足傳入的斷言型接口,就返回true。

15.noneMatch:Stream中沒有元素滿足傳入的斷言型接口,就返回true。

16.generate:接收一個Supplier接口,返回一個Stream,通過實現supplier接口,可以自己來控制流的生成。

17.iterate:

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)

iterate接收兩個參數,第一個是泛型,seed可以理解爲種子值或者起始值。UnaryOperator是一個接口:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T,T>

該接口繼承了Function接口,那麼也必須實現Function接口中的apply方法。除此之外該接口還有一個靜態方法---identity,該方法始終返回其輸入參數。
iterate方法的作用是將種子值成爲stream的第一個元素,f(seed)爲第二個元素,f(f(seed))爲第三個元素,說遞歸你應該比較容易明白。

範例:

Stream.iterate(3, n->n+3).limit(10).forEach(System.out::println);

輸出:

3
6
9
12
15

以上範例中,3即爲種子值,然後f(3)等於6,f(f(3))得9。需要注意的是,iterate方法和generate方法返回的都是無限stream,需要用limite來限制stream的長度。

18.reduce:
reduce提供了三種重載方法。

1. Optional<T>  reduce(BinaryOperator<T> accumulator)
2. T    reduce(T identity, BinaryOperator<T> accumulator)
3. <U> U    reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

第一個方法返回一個Optional對象,接收一個BinaryOperator。我們先來看BinaryOperator是什麼?

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

可以看到BinaryOperator是一個函數型接口,繼承了BiFunction,並且傳入參數和返回值都是相同類型。我們接着看BiFunction的定義:

@FunctionalInterface
public interface BiFunction<T, U, R>{
  R apply(T t, U u);
}

BiFunction接口中有一個apply方法,有兩個參數,一個返回值。到這裏我們大概知道reduce方法傳入的參數大概怎麼用了,在來看返回值Optional的定義:

public final class Optional<T> extends Object

Optional是一個普通的對象,裏面的方法大家可以自己去看API,這裏就不詳細說了。到這裏你可能會說,寫了這麼多,你也沒說reduce到底有什麼作用啊?我們通過名字去猜測一下,reduce有減少,歸納之意。那我們是否可以理解爲,把Stream提供的多個元素歸納成一個對象?

範例:

Integer sum=Stream.of(1,2,3,4).reduce(Integer::sum).get();
        System.out.println(sum);

輸出:

10

我們通過reduce方法,把1-4累加起來得到結果10.你肯定會問爲什麼傳的參數是Integer::sum?我們在以前的文章裏面提到了方法引用::,在這裏就是引用了Integer類的sum方法:

static int sum(int a, int b)  

這個方法是不是就跟BiFunction中定義的apply一樣呢?接收兩個參數和一個返回值。
我們接着看reduce的第二個重載方法,在這個重載方法中多了一個參數T,這就是起始值,然後返回值由Optional變成了T。

範例:

Integer sum=Stream.of(1,2,3,4).reduce(1,Integer::sum);
        System.out.println(sum);

輸出:

11

在此範例中,我們添加了起始值1,使得最後輸出結果多加了1。如果你覺得還不明白,那麼再來看一個例子。

範例:

String sum=Stream.of("a","b","c","d").reduce("1",String::concat);
        System.out.println(sum);

輸出:

1abcd

這會應該明白了。
  關於reduce的第三個重載方法,主要是用於parallelStream的,reduce操作是併發進行的,爲了避免競爭,每個reduce線程都會有獨立的result,combiner參數的作用就是在於合併每個線程的result得到最終的結果。由於第三個方法不是特別常用,我就只說一下方法不給出範例了。

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

這個方法起初一看,頭都大了,這都是什麼鬼?又是U,又是T,又是BiFunction,又是BinaryOperator。BinaryOperator不是繼承與BiFunction的麼?爲什麼不兩個都使用BiFunction呢?

那我們就來解析一下這個方法,首先該方法的返回值是由第一個參數決定的。也就是說第一個參數是什麼類型,該方法就返回什麼類型。這點明確了很重要。

我們接着看第二個參數-BiFunction,爲了理解深刻,我們再次拿出該接口的定義:

@FunctionalInterface
public interface BiFunction<T, U, R>{
  R apply(T t, U u);
}

該接口接收兩個參數,這兩個參數的類型可以不一致。並且返回一個值,值的類型也可以不一致。
接着我們看reduce方法裏面定義的

BiFunction<U,? super T,U> accumulator

我們來對應一下,該接口接收兩個參數,其中第一個爲U,第二個爲T的子類,返回類型爲U。這下就明白多了,也就是說接收兩個不同類型的參數,但是返回值類型跟第一個參數一致,而第一個參數的類型也就是reduce方法的第一個參數類型U。

在看reduce第三個參數-BinaryOperator

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

該接口繼承了BiFunction,但是最重要的是,繼承的BiFunction的兩個接收參數和返回值都是同一個類型T。所以簡單來說BinaryOperator接收兩個參數,返回一個值都是同一類型。

到這裏我們應該明白了爲什麼reduce第二個參數是BiFunction,第三個參數是BinaryOperator了吧?
因爲第二個參數的作用是accumulator,所以接收的兩個參數類型可以不一樣。而前面說了在parallelStream的情況下,combiner的作用是合併每個線程的結果,而每個線程返回的結果都應該是同一個類型,所以在這裏用BinaryOperator而不是BiFunction。

不得不說這種設計真的是太精妙了。

19.collect:
  collect方法跟reduce方法功能很類似,都是聚合方法。不同的是,reduce方法在操作每一個元素時總創建一個新值,而collect方法只是修改現存的值,而不是創建一個新值。

方法定義:

1.<R,A> R collect(Collector<? super T,A,R> collector)  
2.<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)  

這兩個方法都是泛型方法,我們先看第一個。第一個方法接收一個Collector接口作爲參數。如果我們要自己實現它會很麻煩,好在java.util.stream包中給我們提供了一個叫Collectors的類。這個方法我就不在這裏介紹了,大家可以自己去看API,通過Collectors這個類我們可以很容易得到一個Collector對象,這個類中提供了很多統計的操作和創建集合的操作。

範例:

Stream<String> stream = Stream.of("a", "b", "c", "d");
List<String> list =stream.collect(Collectors.toList());
        for (String string : list) {
            System.out.println(string);
        }

輸出:

a
b
c
d

在這裏我們將一個stream流轉換爲了一個List對象。

collect方法的第二種形式跟我們前面說的reduce的很像。接收3個參數,第一個參數是Supplier接口,這個接口我們以前說過。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

BiConsumer接口跟Consumer接口類似,不同的是Consumer接口只接收一個參數而BiConsumer接口接收兩個參數。collect的第二個參數和第三個參數都是BiConsumer接口,但是參數類型卻不一樣。BiConsumer<R,? super T> 第一個參數跟collect返回值一樣,也跟第一個參數一樣。第二個參數類型跟stream的類型一樣。BiConsumer<R,R>則兩個參數類型是相同的。

範例:

System.out.println(Arrays.asList("1","2","3","4").parallelStream().collect(
                StringBuilder::new,
                new BiConsumer<StringBuilder,String>(){

                    @Override
                    public void accept(StringBuilder t, String u) {
                        System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+"   t:"+t+" u:"+u);
                        t.append(u);
                        System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+"   result t:"+t+" u:"+u);
                    }
                    
                }
        , new BiConsumer<StringBuilder,StringBuilder>(){

            @Override
            public void accept(StringBuilder t, StringBuilder u) {
                System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+"   t:"+t+" u:"+u);
                t.append(u);
                System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+"   result t:"+t+" u:"+u);
            }
                            
                        }));

輸出:

accumulator operate current thread:1   t: u:3
accumulator operate current thread:11   t: u:4
accumulator operate current thread:10   t: u:2
accumulator operate current thread:12   t: u:1
accumulator operate current thread:12   result t:1 u:1
accumulator operate current thread:10   result t:2 u:2
accumulator operate current thread:11   result t:4 u:4
accumulator operate current thread:1   result t:3 u:3
combiner operate current thread:1   t:3 u:4
combiner operate current thread:10   t:1 u:2
combiner operate current thread:1   result t:34 u:4
combiner operate current thread:10   result t:12 u:2
combiner operate current thread:10   t:12 u:34
combiner operate current thread:10   result t:1234 u:34
1234

爲了方便大家理解,我並沒有使用lambda表達式。我們首先創建了一個並行的stream,每個stream元素的類型爲String,接着我們調用了collect方法,collect方法第一個參數是創建一個StringBuilder對象,在第二個參數中,我們打印了當前的線程id,和t,u的值方便調試。執行的操作也只是把String加入到StringBuilder中,第三個參數則把兩個StringBuilder合併。從輸出結果中我們可以看見,在執行accumulator操作的時候t的值是空的,並且是4個線程同時進行了accumulator操作,每個線程都把String加入到了StringBuilder中,而在執行combiner操作的時候,就由4個線程變成了2個,然後進行合併操作。最終結果爲1234。由於是多線程的,所以每次輸出的順序是不一樣的。以上輸出只能作爲參考。

到目前爲止,Stream接口中的大部分方法我們都講過了。至於那些IntStream,LongStream都大同小異,大家可以自己去看看,我就不做詳細介紹了。



作者:娃娃要從孩子抓起
鏈接:http://www.jianshu.com/p/b1b7e334ff79
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章