Java 8 stream包初步認識

是什麼?java

這確定是一個咱們最想先了解的問題.git

java.util.stream包是Java 8的新特性之一,雖然名字乍一看好像和io包中的InputStream,OutputStream等很相似,但做用是徹底不一樣的.它的做用是專一於Collection對象進行各類很是便利,高效的聚合操做或者大批量數據操做,支持同爲Java 8新特性的Lambda表達式(可參見另外兩篇Lambda的技術分享文章)以及方法引用(後面使用時一塊兒介紹),極大的提升編程的效率和程序可讀性.而且提供串行執行和並行執行兩種方式進行集合對象的處理,底層採用了Java 7中引入的fork/join框架.這意味着咱們不用去寫一行多線程代碼,就可充分利用多核的性能處理集合對象.而多線程代碼的編寫,偏偏是不容易把握的,須要咱們十分謹慎纔不至於弄巧成拙,Stream的出現無疑是一個好事.github

簡而言之,兩個字,厲害.數據庫

說了一些概念上的東西,想必有些」太長不看」的朋友會對代碼更感興趣,那麼咱們來寫一點代碼,做爲Stream的簡單示例:編程

假設咱們有一個集合,集合中的元素是訂單(Order)對象,每一個訂單包含一個金額(amount)以及一個性別(gender),需求是須要把集合中每一個性別爲男(true)的而且金額大於50000的訂單按照金額從小到大排序,並將最終結果打印出來,按照Java 7的for循環寫法,大體應該是這樣:數組

(for循環寫法太長以致於直接在IDEA中截圖都不方便截完,因此放到了筆記中再次截圖.)多線程

而若是咱們使用Java 8的Stream呢,大概應該是這樣寫:app

上圖的示例中,咱們使用到了Stream API,使用到了Lambda表達式,同時也使用到了方法引用.有沒有感受清爽了不少,沒有那麼多層做用域,代碼量與可讀性都大大提升.框架

固然,咱們也能夠直接從數據庫中使用各類函數及條件,將數據過濾再返回,但這個就是數據庫層面的操做,咱們在此處只區別Java代碼中的區別,就不要在乎這些細節了.dom

經過for循環方式的代碼能夠看到,咱們先遍歷了一次集合,將符合gender爲true且amount>50000的訂單對象取出,放入到一個新的集合中,而後再對集合排序,雖然Java 8已經對底層進行了優化,但排序仍然是再次遍歷了一遍,因此是遍歷了兩遍.

而stream難道說有什麼不一樣嗎?是的,stream只遍歷了一次.雖然咱們調用了filter方法,調用了sorted方法,最終調用collect方法生成了一個List,但並非每調用一個方法就會當即執行操做.這裏咱們引入兩個概念:

l Intermediate(中間操做):一個流能夠後面跟隨零個或多個intermediate操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法,並無真正開始流的遍歷。調用中間操做只會返回一個新的Stream對象.

l Terminal(終端操做):一個流只能有一個terminal操做,當這個操做執行後,流就被消耗掉,沒法再被操做。因此這一定是流的最後一個操做。Terminal操做的執行,纔會真正開始流的遍歷,而且會生成一個結果,或者一個side effect。Side effect在這裏能夠理解爲咱們操做引用對象所形成的結果,雖然沒有返回新的值,但被操做的引用對象可能已經被修改了.調用終端返回的是非Stream對象或者無返回值.

是否是已經感受到了Stream API並不只僅只是對for循環的簡單增強,而是從底層換了另一種方式去實現.這意味着咱們加上再多的Intermediate操做,也只會在執行Terminal操做的時候纔開始遍歷,這樣比傳統的for循環在性能上提高了太多.

怎麼用?

咱們須要先明確一件事,那就是stream操做並非直接操做集合,而是將集合中的元素複製到了一個流中,對流的操做不會影響到原來的集合.

對於如何使用,咱們第一步應該是獲取到一個Stream對象,而後再根據文檔來操做Stream對象.通常來講,最經常使用的一種獲取方式就是如上面示例的方式,調用集合對象的stream方法.

對於數組,咱們可使用Arrays.stream(數組)的方式,將須要得到流的數組傳入,獲得一個流.

以上兩種方法對咱們的基本使用來講已經足夠.

這裏咱們說一說方法引用,簡單理解來講,就是把方法當作參數傳入,而後咱們對流中的每個元素都執行咱們傳入的這個方法.下面的代碼部分可能會用到Lambda表達式以及方法引用.

接下來咱們就須要根據需求去對數據進行處理.

經常使用的Intermediate操做有:

map,filter,distinct,sorted,peek,limit,skip,parallel

從方法名稱來看咱們也大體能夠知道其做用,直接經過代碼示例來看怎麼使用吧.

1.Map

將上游流中的元素,按照給定的邏輯進行處理,返回另外一種類型的元素,而且生成一個新的流,交給後面的操做處理.

能夠看到示例中,咱們調用了map操做,而且經過方法引用的形式傳入了一個參數,最終生成了一個Stream<Integer>對象.在上面代碼中,咱們從randomList中獲取了一個流,而後對流中的每個元素執行了getAmount方法,獲得了一個Integer對象的流.這只是一個簡單的使用,一樣咱們也能夠執行一些比較複雜的操做,好比Order中有name字段,咱們把name做爲key,amount做爲value,將每一個Order對象轉換爲一個Map對象,並生成一個Map的流,這些都是能夠的,你們能夠本身嘗試一下.

map有一些特殊方法.正如咱們所知,裝箱拆箱其實是比較消耗性能的,因此Stream提供了mapToInt,mapToLong,mapToDouble三種方法,用於操做int,long和double,以便於後面的操做可以在性能上獲得優化.

map也有一個flatMap方法,能夠將流中的集合或數組扁平化,例如二維數組,按照通常的map咱們獲得的流中的元素就是一個一個的數組,這可能不怎麼符合咱們一些場景的指望.當咱們須要得到確切的每個最底層元素時,咱們可使用flatMap,將原先的結構扁平化.代碼示例以下:

咱們建立了3個String數組,而後將3個String數組又放入到了一個數組中,由此咱們獲得了一個二維數組.

接下來咱們得到了二維數組的流,經過flatMap方法,將每個流中的元素執行了獲取流的操做,而獲取到的就是元素爲String的流.最終將二維數組的流轉換成爲了一個其底層元素的流.到這裏咱們再簡單的調用一個collect方法,就能夠生成一個集合.

一樣的,flatMap也有三個用於int,long和double的方法,即爲flatMapToInt,flatMapToLong和flatMapToDouble.

2.Filter

見名知意,用於將上游流中的元素按照咱們給定規則進行過濾的操做.

在最上面的示例中,咱們以Lambda表達式的形式傳入了咱們所指定的過濾規則

剛纔演示map操做的時候,都使用的是方法引用的方式,這裏由於判斷條件須要調用兩個方法,將兩個布爾值(布爾表達式)組合爲一個布爾表達式,因此咱們用到了Lambda表達式.

怎麼理解這個表達式呢?

咱們表達式中在箭頭符號左邊寫了一個e,這個e是咱們本身定義的一個變量,改爲別的什麼其實沒差異,它指代的是咱們流中的每個當前元素,能夠直觀的理解爲咱們在遍歷這個集合,而每一個當前元素就是咱們的e,而後返回的值就是箭頭符號右邊的語句的結果.這麼一來咱們這個filter的規則就是:」將每個流中的元素,判斷其gender值是否爲true且amount大於50000」.filter操做期待的返回值是一個布爾值,爲true則代表當前元素符合規則,放入下游流,若是爲false則不符合,繼續判斷下一個.

固然,我這裏所寫的Lambda表達式只是最簡單的一種狀況,其餘的狀況能夠參見另外兩篇文章詳細瞭解.

3.Distinct

   眼熟嗎?是的,就是同一個意思,去重.

這個操做不須要參數,根據Object.equals方法進行判斷.使用起來很簡單,因此就不額外寫代碼演示了,你們能夠本身嘗試一下.

4.Sorted

上面咱們示例中也是用到了的,其實它重載了一個無參的sorted方法,無參的就是按照天然排序來將流中的元素進行排序,但這隻能用於實現了Comparable接口的元素.一樣咱們能夠用上圖示例中的方式,傳入一個自定義的Comparator來實現排序,而咱們所用的方式是Java 8新增的一種方式,簡單理解來講就是根據咱們傳入的方法(獲得用於肯定排序依據的字段),獲得一個Comparator.

上圖即拆開寫的排序,能夠看到實際上咱們sorted方法接收的就是一個Comparator

5.Limit/skip

limit只能傳入一個參數,即指定最多返回多少個元素.

而skip傳入一個參數,則表示跳過前面多少個元素.

這兩個使用起來也是很簡單的,也就不寫代碼作演示了.

6.Peek

這個操做在我第一次看到的時候是以爲難以理解的,主要在於它的使用方法以及返回都和map差很少,對每個傳入的元素進行操做,而後獲得一個流.但後面瞭解了forEach這個Terminal操做以後,發現它其實功能與forEach很像,但差異就在於peek處理以後還會生成一個與上游流元素類型相同的下游流,而forEach做爲一個Terminal操做,會將流消耗掉.

看看源碼註釋中的示例:

除了得到流的方式不一樣之外,其他的操做也基本是前面講到過的,在這個示例中,peek用於將每一個元素進行打印,固然咱們也能夠對每一個元素作一些從新賦值之類的操做.

7.Parallel

這個方法沒有任何參數,用處也就在於咱們前面提到過的,串行與並行.咱們想要得到一個並行流,要麼在建立的時候調用集合的parallelStream獲取,要麼就直接獲取一個stream而後使用parallel操做,將流轉爲並行流.不然咱們得到的都是默認的串行流,而串行與並行的區別能夠看最後的stream的性能介紹.

經常使用的Intermediate操做就是上面所介紹的這些,還有一些其餘的能夠自行去查找資料瞭解.接下來咱們來看看有什麼經常使用的Terminal操做.

1.forEach

這個名字也是很直觀了,跟咱們以前經常使用的foreach差很少,就是遍歷.這個foreach直接使用的話,其實和for循環沒什麼區別,並不神奇.只是它的參數能夠支持Lambda表達式以及方法引用.

這句代碼的效果其實等同於:

這麼一看是否是對方法引用也有了更多的認識?

forEach還有個兄弟,forEachOrdered,名字上多了個ordered,意思就是按照順序進行遍歷,用法沒什麼區別.

2.toArray

這個方法應該是老方法了,能夠直接不傳參使用,獲得一個Object數組,或是傳入一個指定類型的數組,獲得該類型的數組,就不贅述了.

3.Reduce

reduce這個方法,中文意思大概能夠理解爲」聚合」.

這個方法相比其餘方法要來得複雜一些,沒有那麼清新.咱們看看源碼中這個方法的聲明:

看不懂.

看不懂.

更加看不懂了.

那麼咱們直接看看怎麼用的,先用起來再慢慢理解它的含義.

按照第一個方法的傳值,咱們能夠這麼用:

咱們先經過mapToLong將每一個Order對象中的amount拿到,而後經過reduce方法去得到了amount的總和.

能夠看到,reduce方法中咱們實際上傳入了兩個參數,一個是0,一個是Lambda表達式(e1,e2)->e1+e2.

第一個參數0能夠看作咱們求總和時所寫的int sum = 0,做爲初始值,然後面Lambda表達式中的e1,能夠看作是這個reduce方法上一次操做的結果,上一次的e1+e2結果就是這一次的e1,而e2就是當前的元素.而對於第一個元素,能夠理解爲它要執行操做,但沒有可用的e1,就直接跳過第一次操做,把本身做爲了第二次操做的e1,執行第二次操做.

這是有初始值的狀況,咱們再來看看第二個:

這裏我直接把初始值0去掉了,而後最終獲得的結果就再也不是一個long了,而是一個OptionalLong.

這個OptionalLong是什麼東西?其實它也是參照Optional而編寫的一個工具類,只是Optional能夠用於全部類,而OptionalLong只專一於Long類型.而Optional它是由Google的Guava工程得來,在Java 8中新增到java.util包中,用於解決空指針異常.在這裏很少加贅述,感興趣能夠本身查閱資料瞭解更多,咱們在這裏只根據咱們所用到的狀況來簡單說一下.

這個OptionalLong,是對結果值的一個封裝,咱們能夠直接調用getAsLong方法來獲取到實際的結果值,但這樣可能由於沒有值而獲得一個異常,因此咱們換一種方式來獲取,避免沒有值而報錯

有值就返回實際結果值,沒有就返回我傳入的值,這樣就節約了咱們寫if去判斷是否有值,而後再在沒值的狀況下寫邏輯的過程.

爲何會是OptionalLong?由於不一樣於第一個方法,咱們沒有傳入一個初始值,那麼萬一每一個元素都是null的話,最終獲得的值也是null.而第一個方法不管如何也會有個咱們傳入的初始值做爲保底.

而後就到第三個了,最難理解的一個.

這個方法須要區分兩個場景,串行和並行,當串行時第三個參數是不會生效的.

串行時:

對於這個我已經不忍心去改成Lambda表達式了,由於改完是這樣的:

好了,忘掉你看到的東西吧,咱們仍是之前面的圖爲例分析一下.

咱們把整個reduce的參數簡化來看,就是一個orders,一個BiFunction對象,一個BinaryOperator對象.在串行時咱們用不到第三個對象,因此new出來覆寫的時候隨便寫寫吧.

第一個orders,就是初始值.但和咱們第一個方法不一樣,它竟然是個List,而不是相同的Long.

沒錯,第三種寫法能夠用任意類型的初始值,而使用的方法也就在第二個參數中實現.在上面的例子中,咱們用一個List做爲初始值,在new BiFunction對象覆寫其apply方法時,有兩個形參,一個是List,一個是Order.在執行時,list就是咱們前面傳入的初始值list,而order就是遍歷的當前元素,在這裏咱們只是簡單的將order放入了list中,你也能夠根據本身的需求作其餘的操做.若是初始值是同類型的,那寫法就跟咱們第一種寫法同樣了.因此,第二個參數就是負責咱們怎麼將流中元素與傳入的初始值進行處理.

並行時:

我對第三個參數的apply方法邏輯進行了修改,將傳入的orders2集合全部元素徹底放入orders集合中.

剛纔咱們說到,第三個參數在並行時纔有用,是由於並行時有多個線程,每一個線程都會有一個List<Order>,最終咱們須要將全部的list合併才能獲得完整的結果,因此第三個參數的做用就是這個.

有一個點須要注意,這種操做可能會出現一個問題,就是咱們初始值若是不是一個空集合,每一個並行線程都會在此基礎上繼續放入Order對象,那最終合併後咱們拿到的結果集合就會有多個初始值中的元素.

4.Collect

collect方法就是將流最終生成一個集合或者Map,將流中的元素」收集」起來.在最上面的示例中,咱們也用到了collect方法做爲Terminal操做.

collect有兩種,一種接收3個參數,supplier,accumulator,combiner.還有一種是接收一個Collector對象.咱們先來看看這個3參數的collect方法:

仍是先貼完整寫法的代碼:

這個完整寫法其實和reduce的第三個方法有點類似的,咱們來細看一下.

第一個參數,new了一個Supplier,泛型爲HashSet<Order>.Supplier意思是供應商,而我覆寫了其中的get邏輯,new了一個HashSet並返回出去.這其實就是咱們提供了一個」獲取初始值」的對象,和reduce類似卻又不徹底相同,都是須要初始值,但reduce是直接提供,這裏是提供一個生成初始值的對象.

第二個參數和第三個參數,咱們按照reduce第三個方法的思路來猜想一下,首先第二個參數,我new了一個BiConsumer,起手感受就不太好,類型都跟reduce第二個參數不同啊.不慌,看下覆寫的方法是什麼參數.accept方法,接收一個HashSet,一個Order,那這個HashSet應該就是經過咱們提供的Supplier生成的初始值了,而後後面的order對象應該是咱們流中的元素.

第二個參數提供的方法,與reduce同樣,會被重複調用直至流中的元素所有被消耗.

第三個,類型和第二個參數同樣,但接收了兩個HashSet,聯想到reduce,這是用於並行處理時,最終合併結果.

固然,此處咱們使用的是HashSet做爲示例,其餘的集合也是可使用的.

而後仍是貼一下使用方法引用的寫法,多作比較對理解也有好處:

其實與上面的完整寫法一比較,方法引用的寫法也就沒有那麼難看懂了.

再說一下collect傳Collector對象的方法,這個方法沒什麼多說的,但內容基本就在於生成Collector對象的Collectors類上.咱們先看一看Collectors有哪些方法能夠用於生成Collector:

以上是用於生成集合,或者groupingBy生成Map對象.也有一些其餘的方法,例如:

能夠經過傳入的字符將全部結果的toString值拼接起來,這個用法相信你們也是比較熟悉的.

以上分別是計數,最小值,最大值,Int求和,long求和,double求和.

可見Collectors這個類中提供了不少功能,咱們在這裏也就不一一細講,有興趣能夠下來本身瞭解.

那麼咱們如何經過Collectors去改造咱們前面的複雜寫法呢?

就這樣,就能夠根據咱們所選擇生成的Collector來獲得最終的」收集」結果了.固然,自己要從List轉Set不用這麼寫,這只是爲了舉個例子,不要在乎這些細節.

5.min/max/count

這些操做是幹嗎的想必不用多說了吧.使用也是常規操做,min/max傳入一個Comparator,count不須要參數.

6.anyMatch/allMatch/noneMatch/findFirst/findAny

要把他們放一塊兒說,是由於他們有一點點特殊.咱們都知道」&&」還有」||」運算符,他們都有短路的效果,以上方法也是一樣的,一旦符合短路條件就不會遍歷後面的元素.

anyMatch:任一符合給定判斷條件就短路,返回一個true,

allMatch:所有符合才返回一個true,一旦有一個不符合就短路

noneMatch:所有不符合才返回一個true,一旦有一個符合就短路

findFirst:找到符合條件的第一個元素就短路,立刻返回

findAny:找到符合條件的任意一個就短路,立刻返回.

好了,至此Stream中的經常使用方法也大體介紹了一下.接下來是關於Stream性能的討論.

stream的性能

在我最開始查找資料瞭解Stream性能的時候,看到一個文章

看到標題的時候內心是咯噔一下的,用起來這麼爽(這麼裝逼)的一個東西,竟然性能比for-loop差了不止一點半點,而是相差5倍.

而後那幾天是一點都不開心的,直到下面的評論多起來,以及gitHub上面一個更爲可靠的測試出現,客觀的評價了stream的性能.

的確,stream並非無腦高性能,但也不至於慢5倍.

先貼上gitHub的帖子地址:(中文的,不要怕)

https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md

固然,對於」太長不看」的朋友,我這裏大體總結一下:

1.對於簡單操做,例如簡單遍歷,for-loop的性能高於串行stream,但並行stream會由於CPU核數提升而性能也隨之提升.

2.對於複雜操做,例如map,filter一系列的操做,串行stream能夠和高質量的for-loop性能至關,而此時並行stream由於CPU核數以及數據量的提升而性能碾壓for-loop

3.不建議在單核環境下使用並行stream.

4.使用stream能夠在底層代碼有所優化時,咱們無須做出任何調整,這也是」依賴接口而不依賴實現」的優點所在

5.儘可能消除掉自動裝拆箱,這樣在後續的大量操做中可以節省很多的時間.而消除能夠經過mapToInt,mapToLong和mapToDouble這些操做來實現.

相關文章
相關標籤/搜索