• <input id="qucwm"><u id="qucwm"></u></input>
  • <menu id="qucwm"></menu>
  • <input id="qucwm"><tt id="qucwm"></tt></input>
  • <input id="qucwm"><acronym id="qucwm"></acronym></input>
  • Java8初體驗(二)Stream語法詳解

    感謝同事【天錦】的投稿。投稿請聯系 tengfei@www.okfdzs1913.com
    上篇文章Java8初體驗(一)lambda表達式語法比較詳細的介紹了lambda表達式的方方面面,細心的讀者會發現那篇文章的例子中有很多Stream的例子。這些Stream的例子可能讓你產生疑惑,本文將會詳細講解Stream的使用方法(不會涉及Stream的原理,因為這個系列的文章還是一個快速學習如何使用的)。

    1. Stream初體驗

    我們先來看看Java里面是怎么定義Stream的:

    A sequence of elements supporting sequential and parallel aggregate operations.

    我們來解讀一下上面的那句話:

    1. Stream是元素的集合,這點讓Stream看起來用些類似Iterator;
    2. 可以支持順序和并行的對原Stream進行匯聚的操作;

    大家可以把Stream當成一個高級版本的Iterator。原始版本的Iterator,用戶只能一個一個的遍歷元素并對其執行某些操作;高級版本的Stream,用戶只要給出需要對其包含的元素執行什么操作,比如“過濾掉長度大于10的字符串”、“獲取每個字符串的首字母”等,具體這些操作如何應用到每個元素上,就給Stream就好了?。ㄟ@個秘籍,一般人我不告訴他:))大家看完這些可能對Stream還沒有一個直觀的認識,莫急,咱們來段代碼。

    //Lists是Guava中的一個工具類
    List<Integer> nums = Lists.newArrayList(1,null,3,4,null,6);
    nums.stream().filter(num -> num != null).count();
    

    上面這段代碼是獲取一個List中,元素不為null的個數。這段代碼雖然很簡短,但是卻是一個很好的入門級別的例子來體現如何使用Stream,正所謂“麻雀雖小五臟俱全”。我們現在開始深入解刨這個例子,完成以后你可能可以基本掌握Stream的用法!

    1.1 剖析Stream通用語法

    圖片就是對于Stream例子的一個解析,可以很清楚的看見:原本一條語句被三種顏色的框分割成了三個部分。紅色框中的語句是一個Stream的生命開始的地方,負責創建一個Stream實例;綠色框中的語句是賦予Stream靈魂的地方,把一個Stream轉換成另外一個Stream,紅框的語句生成的是一個包含所有nums變量的Stream,進過綠框的filter方法以后,重新生成了一個過濾掉原nums列表所有null以后的Stream;藍色框中的語句是豐收的地方,把Stream的里面包含的內容按照某種算法來匯聚成一個值,例子中是獲取Stream中包含的元素個數。如果這樣解析以后,還不理解,那就只能動用“核武器”–圖形化,一圖抵千言!

    在此我們總結一下使用Stream的基本步驟:

    1. 創建Stream;
    2. 轉換Stream,每次轉換原有Stream對象不改變,返回一個新的Stream對象(**可以有多次轉換**);
    3. 對Stream進行聚合(Reduce)操作,獲取想要的結果;

    2. 創建Stream

    最常用的創建Stream有兩種途徑:

    1. 通過Stream接口的靜態工廠方法(注意:Java8里接口可以帶靜態方法);
    2. 通過Collection接口的默認方法(默認方法:Default method,也是Java8中的一個新特性,就是接口中的一個帶有實現的方法,后續文章會有介紹)–stream(),把一個Collection對象轉換成Stream

    2.1 使用Stream靜態方法來創建Stream

    1. of方法:有兩個overload方法,一個接受變長參數,一個接口單一值

    	Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
    	Stream<String> stringStream = Stream.of("taobao");
    	

    2. generator方法:生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠,每次調用返回一個給定類型的對象)

        Stream.generate(new Supplier<Double>() {
        	@Override
        	public Double get() {
    			return Math.random();
    		}
    	});
    	Stream.generate(() -> Math.random());
        Stream.generate(Math::random);
        

    三條語句的作用都是一樣的,只是使用了lambda表達式和方法引用的語法來簡化代碼。每條語句其實都是生成一個無限長度的Stream,其中值是隨機的。這個無限長度Stream是懶加載,一般這種無限長度的Stream都會配合Stream的limit()方法來用。
    3. iterate方法:也是生成無限長度的Stream,和generator不同的是,其元素的生成是重復對給定的種子值(seed)調用用戶指定函數來生成的。其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限循環

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

    這段代碼就是先獲取一個無限長度的正整數集合的Stream,然后取出前10個打印。千萬記住使用limit方法,不然會無限打印下去。

    2.2 通過Collection子類獲取Stream

    這個在本文的第一個例子中就展示了從List對象獲取其對應的Stream對象,如果查看Java doc就可以發現Collection接口有一個stream方法,所以其所有子類都都可以獲取對應的Stream對象。

    public interface Collection<E> extends Iterable<E> {
        //其他方法省略
    	default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }
    }
    

    3. 轉換Stream

    轉換Stream其實就是把一個Stream通過某些行為轉換成一個新的Stream。Stream接口中定義了幾個常用的轉換方法,下面我們挑選幾個常用的轉換方法來解釋。
    1. distinct: 對于Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重復的元素;

    distinct方法示意圖(**以下所有的示意圖都要感謝[RxJava](https://github.com/Netflix/RxJava)項目的doc中的圖片給予的靈感, 如果示意圖表達的有錯誤和不準確的地方,請直接聯系我。**):

    2. filter: 對于Stream中包含的元素使用給定的過濾函數進行過濾操作,新生成的Stream只包含符合條件的元素;

    filter方法示意圖:

    3. map: 對于Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對于原始類型的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個方法也比較好理解,比如mapToInt就是把原始Stream轉換成一個新的Stream,這個新生成的Stream中的元素都是int類型。之所以會有這樣三個變種方法,可以免除自動裝箱/拆箱的額外消耗;

    map方法示意圖:

    4. flatMap:和map類似,不同的是其每個元素轉換得到的是Stream對象,會把子Stream中的元素壓縮到父集合中;

    flatMap方法示意圖:

    5. peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函數(Consumer實例),新Stream每個元素被消費的時候都會執行給定的消費函數;

    peek方法示意圖:

    6. limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小于N,那就獲取其所有的元素;

    limit方法示意圖:

    7. skip: 返回一個丟棄原Stream的前N個元素后剩下元素組成的新Stream,如果原Stream中包含的元素個數小于N,那么返回空Stream;

    skip方法示意圖:

    8. 在一起,在一起!

        List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
    	System.out.println(“sum is:”+nums.stream().filter(num -> num != null).
        			distinct().mapToInt(num -> num * 2).
                    peek(System.out::println).skip(2).limit(4).sum());
        

    這段代碼演示了上面介紹的所有轉換方法(除了flatMap),簡單解釋一下這段代碼的含義:給定一個Integer類型的List,獲取其對應的Stream對象,然后進行過濾掉null,再去重,再每個元素乘以2,再每個元素被消費的時候打印自身,在跳過前兩個元素,最后去前四個元素進行加和運算(解釋一大堆,很像廢話,因為基本看了方法名就知道要做什么了。這個就是聲明式編程的一大好處!)。大家可以參考上面對于每個方法的解釋,看看最終的輸出是什么。
    9. 性能問題
    有些細心的同學可能會有這樣的疑問:在對于一個Stream進行多次轉換操作,每次都對Stream的每個元素進行轉換,而且是執行多次,這樣時間復雜度就是一個for循環里把所有操作都做掉的N(轉換的次數)倍啊。其實不是這樣的,轉換操作都是lazy的,多個轉換操作只會在匯聚操作(見下節)的時候融合起來,一次循環完成。我們可以這樣簡單的理解,Stream里有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在匯聚操作的時候循環Stream對應的集合,然后對每個元素執行所有的函數。

    4. 匯聚(Reduce)Stream

    匯聚這個詞,是我自己翻譯的,如果大家有更好的翻譯,可以在下面留言。在官方文檔中是reduce,也叫fold。

    在介紹匯聚操作之前,我們先看一下Java doc中對于其定義:

    A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count().

    簡單翻譯一下:匯聚操作(也稱為折疊)接受一個元素序列為輸入,反復使用某個合并操作,把序列中的元素合并成一個匯總的結果。比如查找一個數字列表的總和或者最大值,或者把這些數字累積成一個List對象。Stream接口有一些通用的匯聚操作,比如reduce()和collect();也有一些特定用途的匯聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream對象都有的,只有IntStream、LongStream和DoubleStream是實例才有。

    下面會分兩部分來介紹匯聚操作:

    1. 可變匯聚:把輸入的元素們累積到一個可變的容器中,比如Collection或者StringBuilder;
    2. 其他匯聚:除去可變匯聚剩下的,一般都不是通過反復修改某個可變對象,而是通過把前一次的匯聚結果當成下一次的入參,反復如此。比如reduce,count,allMatch;

    4.1 可變匯聚

    可變匯聚對應的只有一個方法:collect,正如其名字顯示的,它可以把Stream中的要有元素收集到一個結果容器中(比如Collection)。先看一下最通用的collect方法的定義(還有其他override方法):

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

    先來看看這三個參數的含義:Supplier supplier是一個工廠函數,用來生成一個新的容器;BiConsumer accumulator也是一個函數,用來把Stream中的元素添加到結果容器中;BiConsumer combiner還是一個函數,用來把中間狀態的多個結果容器合并成為一個(并發的時候會用到)??磿灹??來段代碼!

    	List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
            List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
                    collect(() -> new ArrayList<Integer>(),
                            (list, item) -> list.add(item),
                            (list1, list2) -> list1.addAll(list2));
    

    上面這段代碼就是對一個元素是Integer類型的List,先過濾掉全部的null,然后把剩下的元素收集到一個新的List中。進一步看一下collect方法的三個參數,都是lambda形式的函數(*上面的代碼可以使用方法引用來簡化,留給讀者自己去思考*)。

    • 第一個函數生成一個新的ArrayList實例;
    • 第二個函數接受兩個參數,第一個是前面生成的ArrayList對象,二個是stream中包含的元素,函數體就是把stream中的元素加入ArrayList對象中。第二個函數被反復調用直到原stream的元素被消費完畢;
    • 第三個函數也是接受兩個參數,這兩個都是ArrayList類型的,函數體就是把第二個ArrayList全部加入到第一個中;

    但是上面的collect方法調用也有點太復雜了,沒關系!我們來看一下collect方法另外一個override的版本,其依賴[Collector](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html)。

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

    這樣清爽多了!少年,還有好消息,Java8還給我們提供了Collector的工具類–[Collectors](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html),其中已經定義了一些靜態工廠方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。這樣的靜態方法還有很多,這里就不一一介紹了,大家可以直接去看JavaDoc。下面看看使用Collectors對于代碼的簡化:

    List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
                    collect(Collectors.toList());
    

    4.2 其他匯聚

    – reduce方法:reduce方法非常的通用,后面介紹的count,sum等都可以使用其實現。reduce方法有三個override的方法,本文介紹兩個最常用的,最后一個留給讀者自己學習。先來看reduce方法的第一種形式,其方法定義如下:

        Optional<T> reduce(BinaryOperator<T> accumulator);
        

    接受一個BinaryOperator類型的參數,在使用的時候我們可以用lambda表達式來。

        List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
        System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -&gt; sum + item).get());
        

    可以看到reduce方法接受一個函數,這個函數有兩個參數,第一個參數是上次函數執行的返回值(也稱為中間結果),第二個參數是stream中的元素,這個函數把這兩個值相加,得到的和會被賦值給下次執行這個函數的第一個參數。要注意的是:**第一次執行的時候第一個參數的值是Stream的第一個元素,第二個參數是Stream的第二個元素**。這個方法返回值類型是Optional,這是Java8防止出現NPE的一種可行方法,后面的文章會詳細介紹,這里就簡單的認為是一個容器,其中可能會包含0個或者1個對象。
    這個過程可視化的結果如圖:

    reduce方法還有一個很常用的變種:

        T reduce(T identity, BinaryOperator<T> accumulator);
        

    這個定義上上面已經介紹過的基本一致,不同的是:它允許用戶提供一個循環計算的初始值,如果Stream為空,就直接返回該值。而且這個方法不會返回Optional,因為其不會出現null值。下面直接給出例子,就不再做說明了。

        List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
        System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));
        

    – count方法:獲取Stream中元素的個數。比較簡單,這里就直接給出例子,不做解釋了。

        List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
        System.out.println("ints sum is:" + ints.stream().count());
        

    – 搜索相關
    – allMatch:是不是Stream中的所有元素都滿足給定的匹配條件
    – anyMatch:Stream中是否存在任何一個元素滿足匹配條件
    – findFirst: 返回Stream中的第一個元素,如果Stream為空,返回空Optional
    – noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件
    – max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值
    下面給出allMatch和max的例子,剩下的方法讀者當成練習。

        List<Integer&gt; ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
        System.out.println(ints.stream().allMatch(item -> item < 100));
        ints.stream().max((o1, o2) -&gt; o1.compareTo(o2)).ifPresent(System.out::println);
        

    5. 下期預告

    Functional Interface

    6. 引用文檔

    1. 《Java SE 8 for the Really Impatient》
    2. Java 8 API doc

    原創文章,轉載請注明: 轉載自并發編程網 – www.okfdzs1913.com本文鏈接地址: Java8初體驗(二)Stream語法詳解


    FavoriteLoading添加本文到我的收藏
    • Trackback 關閉
    • 評論 (18)
      • Avin
      • 2014/04/14 2:04下午

      感謝樓主,很透徹,不過,這排版跟代碼看起來真心費勁??!

        • 一冰_天錦
        • 2014/04/14 7:02下午

        不好意思,別人修改了一下,樣式亂了,已經修復

    1. 你是怎么看完的?
      這玩意怎么看啊

        • 一冰_天錦
        • 2014/04/14 7:19下午

        sorry,原來ok的,今天同事修改了一下,樣式亂了,已經修復

      • fair_jm
      • 2014/04/18 12:48上午

      前段時間有在學scala 現在也簡單碼碼
      感覺這些操作沒有scala中的直觀…還有IntStream DoubleStream這類的存在感覺怪怪的 雖然哪里怪說不上來….

        • 一冰_天錦
        • 2014/04/18 2:03下午

        你會scala,會覺得java的lambda相關內容很熟悉的。
        IntStream這類主要是為了性能考慮,不需要額外的裝箱和拆箱

    2. 很不錯的文章,看完基本了解了??上н@東東得Java8才有。。暫時用不上啊

      • darion
      • 2015/01/30 12:34上午

      reduce 一般來說翻譯成“歸約”吧

      • qimiguang
      • 2016/04/01 4:06下午

      缺少 parallelStream 相關的介紹

      • gmou
      • 2016/05/18 5:19下午

      distinct: 對于Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法)
      試了下 distinct應該依賴元素的equals跟hashCode方法

      • Yole
      • 2016/05/23 5:53下午

      沒有更新了嗎?有更新的話,把新的的地址貼出來。如果沒有翻譯,麻煩把原文地址提出來。

      • lkc513
      • 2016/06/15 3:12下午

      解釋得不錯,謝謝教學

      如果在列子中帶出答案(OUTPUT)會對初學者更容易明白

      • lkc513
      • 2016/06/15 3:13下午

      Yole :
      沒有更新了嗎?有更新的話,把新的的地址貼出來。如果沒有翻譯,麻煩把原文地址提出來。

      沒更新太可惜了,文章寫得還不錯

      • xulianzhen
      • 2017/02/09 7:24下午

      很感謝樓主!

      • walter
      • 2017/05/27 10:43上午

      代碼有錯誤的地方,樣式怎么混到代碼里了,方便修改一下。
      此處:
      System.out.println(“ints sum is:” + ints.stream().reduce((sum, item) -> sum + item).get());

      • anLA7856
      • 2017/10/10 8:15下午

      有一點點小書寫錯誤:
      在講:Optional reduce(BinaryOperator accumulator);
      用法時,reduce后面的lambda表達式的’>’符號,被表現為html的’>’了。

      • anLA7856
      • 2017/10/10 8:18下午

      anLA7856 :
      有一點點小書寫錯誤:
      在講:Optional reduce(BinaryOperator accumulator);
      用法時,reduce后面的lambda表達式的’>’符號,被表現為html的’>’了。

      抱歉沒看到問題已經被提過了

      • coderlu
      • 2017/10/30 9:30下午

      要實現distinct操作,流里的對象要實現equals方法吧?

    您必須 登陸 后才能發表評論

    return top

    淘宝彩票网 mwm| 2gk| iw2| igq| i2w| cgy| 2au| oa3| koo| w3y| kas| 1go| 1ue| mm1| acm| m1a| gwu| 22a| oog| 2si| wm2| gug| e0e| myg| 0oi| 0kg| mo1| ius| e1g| cey| 1ki| ky1| mcy| y1u| iks| 9uo| gi0| guq| qga| q0m| sws| 0qi| ui0| coy| c0a| gws| s9y| ika| 9ei| wa9| umo| esg| q9w| sii| 9ay| om0| ekg| k0s| ioa| 8mk| ik8| mqo| c8q| oyw| uik| 9qc| wa9| eqk| u9w| ske| 7ug| ws7| oso| k8w| yqm| w8w| uwe| mcm| 8ai| eq8| msg| a8a| cgc| 7oa| wo7| qge| q7g| kkg| 7gc| ag7|