談一談Java8的函數語言程式設計(二)——Java8中的流
流與集合
眾所周知,日常開發與操作中涉及到集合的操作相當頻繁,而java中對於集合的操作又是相當麻煩。這裡你可能就有疑問了,我感覺平常開發的時候操作集合時不麻煩呀?那下面我們從一個例子說起。
計算從倫敦來的藝術家的人數
請注意這個問題例子在本篇部落格中會經常提到,希望你能記住這個簡單的例子
這個問題看起來相當的簡單,那麼使用for迴圈進行計算
int count=0; for(Artist artist: allArtists){ if(artisst.isFrom("London")){ count++;
標準的寫法如上圖,當然是沒有問題的了,儘管這樣的操作是可以的,但依舊存在著問題。
每次需要迭代集合類的的時候,我都要寫這樣的5行程式碼或者更多,並且將這樣的程式碼想要改成並行執行的方式也十分的麻煩,需要修改每個for迴圈才能夠實現。
第二個問題就是在於這樣的寫法本身就是閱讀性很差的,什麼?我很容易就看的懂呀,但事實上,你不得不承認,其他人必須要閱讀了整個迴圈體,然後再思考一會,才能得出:哦!這段程式碼是做這個的,當然了,這個例子相當簡單,你可能幾秒鐘就看理解了,但是面對一個多層迴圈巢狀的集合迭代操作,想看明白,那就相當頭疼了。
第三個問題在於,for迴圈從本質上來講是一種序列化的操作,從總體來看的話,使用for迴圈會將行為和方法混為一談。
外部迭代與內部迭代
上文所用到的for迴圈是來自java5的增強for迴圈,本質上是屬於iterator迭代器的語法糖,這種使用迭代器的迭代集合的方式,稱之為外部迭代,說的通俗一點,就是需要我們程式猿手動的對這個集合進行種種操才能得到想要結果的迭代方式,叫做外部迭代。
與外部迭代所對應的,則是內部迭代,內部迭代與之相反,是集合本身內部通過流進行了處理之後,程式猿們只需要直接取結果就行了,這種迭代稱為內部迭代。
那麼問題來了,用內部迭代怎麼解決上面的問題呢?
long count=allArtists.stream()//進行流操作
ok,也許你還暫時還不瞭解關於stream()流的相關操作,彆著急,下文會對這些api語法作說明。與上文對應,這裡同樣針對上文列舉出三條好處。
每次需要迭代的時候,並不需要寫同樣的程式碼塊,說出來你可能不信,這樣的程式碼只有一行,分成三行來表示只是為了方便閱讀,改成並行操作的方式也簡單的驚人,只需要將第一行的stream()改為parallelStream()就可以了
第二個好處就可閱讀行性,相信你即你現在暫時不懂得流的相關api,也能看懂上文的操作,仔細想想這個問題:計算從倫敦來的藝術家的人數,那不就是兩步嗎?第一步篩選出所有來自倫敦的藝術家,第二步統計他們的人數,現在你回頭看上文的程式碼,第一行使用流對集合進行內部操作,第二步篩選出來自倫敦的藝術家,第三步計數,簡單明瞭,沒有令人頭疼的迴圈,也不需要看完整段程式碼才理解這一行是做什麼的。
第三個好處其實第一點已經提到的,輕鬆的並行化,並且既然是涉及到集合的相關操作,就讓集合自己去完成,何必勞駕寶貴的程式設計師的其他時間呢?
常用流的api
1.獲取流物件、
要進行相應的流操作,必然要先獲得流物件,首先介紹的就是如何獲得一個流的物件。
對於集合來說,直接通過stream()方法即可獲取流物件
List list=new ArrayList();
對於陣列來說,通過Arrays類提供的靜態函式stream()獲取陣列的流物件
String[] names={"chaimm","peter","john"};
直接將幾個普通的數值變成流物件
Stream stream=Stream.of("chaimm","peter","john");
2.collect(toList())
collect(Collectors.toList())方法是將stream裡的值生成一個列表,也就是將流再轉化成為集合,是一個及早求值的操作。
關於惰性求值與及早求值,這裡簡單說明一下,這兩者最重要的區別就在於看操作有沒有具體的返回值(或者說產生了具體的數值),比如上文的的 統計來自英國藝術家人數 的程式碼,第二行程式碼的操作是首先篩選出來自英國的藝術家,這個操作並沒有實際的數值產生,因此這個操作就是惰性求值,而最後的count計數方法,產生了實際的數值,因此是及早求值。惰性求值是用於描述stream流的,因此返回值是stream,而幾乎所有對於流的鏈式操作都是進行各種惰性求值的鏈式操作,最後加上一個及早求值的方法返回想要的結果。
你可以用建造者的設計模式去理解他,建造者模式通過一系列的操作進行設定與配置操作,最後呼叫一個build方法,創建出相應的物件。對於這裡也是同樣,呼叫各種惰性求值的方法,返回一個stream流,最後一步呼叫一個及早求值的方法,得到最終的結果。
那麼現在對於這個collect(toList()),使用方法就十分明瞭了。
list.stream()//將集合轉化成流
3.篩選filter
你如果有耐心,看到了這裡對於這個操作應該不陌生了,filter函式接收一個Lambda表示式作為引數,該表示式返回boolean,在執行過程中,流將元素逐一輸送給filter,並篩選出執行結果為true的元素。
還是上文的例子:篩選出來自英國的藝術家
long count=allArtists.stream()
4.去重distinc
long count=allArtists.stream()
這樣只增加了一行,便達到了篩選出所有來自英國的藝術家,並且去掉重複的名字之後的統計數量的目的
你看,符合了上文所說的,簡單,易懂,可讀性強。
相信下面我說的幾個方法你一看就懂。
5.擷取limit
擷取流的前N個元素
long count=allArtists.stream()
6.跳過skip
跳過流的前N個元素:
long count=allArtists.stream()
7.對映map
如果有一個函式可以將一種型別的值轉換成另外一種型別,map操作就可以使用該函式,將一個流中的值轉換成一個新的流。
對映這個操作其實在大家程式設計的過程中都經常用到,也就是將A對映成B A->B
還是用藝術家的例子,現在要獲得一個包含所有來自倫敦藝術家的名字的集合
List artistNames=allArtists.stream()
請注意,這裡的傳遞的Lambda表示式必須是Function介面的一個例項,Function介面是隻包含一個引數 的普通函式介面。
8.flatMap
上一條已經介紹過map操作,它可以用一個新的值代替stream裡的值,但有時候,使用者希望讓map操作有點變化,生成一個新的steram物件取而代之,使用者通常不希望結果是一連串的流,此時flatMap能夠派上用場。
通俗的一點的說法是,他可以將一條一條的小流,匯聚成一條大流,好比海納百川的感覺。
用一個簡單的例子就很容易理解了
假設有一個包含多個集合的流,現在希望得到所有數字的序列,利用flatMap解決辦法如下
List together=Stream.of(asList(1,2),asList(3,4))
輸出結果為1,2,3,4
你看,2條小流被整合成了一條流!(這就是為什麼這個類庫叫做stream,流的意思,十分的形象化)
steram流,在java8裡,你可以理解成流水線,流水線的上的商品就是集合裡一個個的元素,而這些對於流的各種各樣的流操作,就是流水線上加工這些商品的機器。所以呢,stream流的相關特性與之也符合
不可逆,無論是河流,水流,還是流水線,沒聽過有倒流的,因此java8中的流也同樣如此,你不可能在操作完第一個元素之後回頭再重新操作,這在流操作裡是無法完成的。
另一個特性就是內部迭代,這在一開始已經講述過了。
為什麼到這裡我才做不可逆的特性說明呢,因為我覺得flatMap很能符合流的特點,水流嘛,海納百川,不可逆流,你看,完美符合java8的流特性。
9.max和min
例子: 獲得所有藝術家中,年齡最大的藝術家
想一想,採用原始的外部迭代,要達到這麼簡單的要求是不是忽然感覺很麻煩?排個序?還是寫一個交替又或者是選擇比較的演算法?何必這麼麻煩!使用流操作採用內部迭代就好了,這不是我們程式猿應該專門寫一段外部程式來解決的問題!
Stream上常用的操作之一是求最大值和最小值,事實上通過流操作來完成最大值最小值的方式有很多很多種,這裡介紹的max和min的方法是stream類裡的直接附帶的方法,事實上在實際操作的時候我並不會選擇這種操作方式( 關於這點,在後面的章節會提到,這裡提前做一個記號,以後增加超連結過去 )
使用流操作如下:
Artist theMaxAgeArtist=allArtists.stream()
我們一行一行地說
第一行,轉化為流物件,讀到這裡的你相信已經十分理解了,因此以後對於這一行不再說明了
第二行,查詢Stream中最大或最小的元素,首先要考慮的是用什麼作為排序的條件,這裡顯然是根據藝術家的年齡作為指標,為了讓Stream物件按照藝術家的年齡進行排序,需要傳給它一個Comparator物件,java8提供了一個新的靜態方法comparing,使用它可以方便的實現一個比較器。放在以前,我們需要比較兩個物件的某個屬性的值,現在只需要提供一個get方法就可以了。
這個comparing方法很有意思,這個方法接受一個函式作為引數,並且返回另一個函式。這在其他語言裡聽起來像是廢話,然而在java裡可不能這麼認為,這種方法早就該引入Java的標準類庫,然而之前的實現方式只能是匿名內部類的實現,無論是看起來,還是寫起來,都是相當的難受,所以一直就沒有實現,但是現在有了Lambda表示式,就變得很簡介啦。
第三行,max()方法返回的是一個Optional物件,這個物件對我們來說會是有點陌生,下一條我會專門對這個物件進行介紹,在這裡需要記住的是,通過get方法可以去除Optional物件中的值。
10.Optional物件
Optional是Java8新加入的一個容器,這個容器只存1個或0個元素,它用於防止出現NullpointException,它提供如下方法:
isPresent()
判斷容器中是否有值。
ifPresent(Consume lambda)
容器若不為空則執行括號中的Lambda表示式。
T get()
獲取容器中的元素,若容器為空則丟擲NoSuchElement異常。
T orElse(T other)
獲取容器中的元素,若容器為空則返回括號中的預設值。
值得注意的是,Optional物件不僅可以用於新的Java 8 API,也可用於具體領域類中,和普通的類並沒有什麼區別,當試圖避免空值相關的缺陷,如捕獲的異常時,可以考慮一下是否可使用Optional物件。
本篇小結
本篇以一個藝術家的例子介紹了流與基本流的相關操作,目的是為了讓看到本篇部落格的人嘗試著使用這樣的函式式方法,並開始理解什麼是java8中的流。