流與集合 眾所周知,日常開發與操作中涉及到集合的操作相當頻繁,而java中對於集合的操作又是相當麻煩。這裡你可能就有疑問了,我感覺平常開發的時候操作集合時不麻煩呀?那下麵我們從一個例子說起。 計算從倫敦來的藝術家的人數 請註意這個問題例子在本篇博客中會經常提到,希望你能記住 ...
流與集合
眾所周知,日常開發與操作中涉及到集合的操作相當頻繁,而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()//進行流操作
.filter(artist -> artist.isFrom("London"))//選出所有來自倫敦的藝術家
.count();//統計他們的數量
ok,也許你還暫時還不瞭解關於stream()流的相關操作,彆著急,下文會對這些api語法作說明。與上文對應,這裡同樣針對上文列舉出三條好處。
- 每次需要迭代的時候,並不需要寫同樣的代碼塊,說出來你可能不信,這樣的代碼只有一行,分成三行來表示只是為了方便閱讀,改成並行操作的方式也簡單的驚人,只需要將第一行的stream()改為parallelStream()就可以了
- 第二個好處就可閱讀行性,相信你即你現在暫時不懂得流的相關api,也能看懂上文的操作,仔細想想這個問題:計算從倫敦來的藝術家的人數,那不就是兩步嗎?第一步篩選出所有來自倫敦的藝術家,第二步統計他們的人數,現在你回頭看上文的代碼,第一行使用流對集合進行內部操作,第二步篩選出來自倫敦的藝術家,第三步計數,簡單明瞭,沒有令人頭疼的迴圈,也不需要看完整段代碼才理解這一行是做什麼的。
- 第三個好處其實第一點已經提到的,輕鬆的並行化,並且既然是涉及到集合的相關操作,就讓集合自己去完成,何必勞駕寶貴的程式員的其他時間呢?
常用流的api
1.獲取流對象、
要進行相應的流操作,必然要先獲得流對象,首先介紹的就是如何獲得一個流的對象。
對於集合來說,直接通過stream()方法即可獲取流對象
List<Person> list = new ArrayList<Person>(); Stream<Person> stream = list.stream();
對於數組來說,通過Arrays類提供的靜態函數stream()獲取數組的流對象
String[] names = {"chaimm","peter","john"}; Stream<String> stream = Arrays.stream(names);
直接將幾個普通的數值變成流對象
Stream<String> stream = Stream.of("chaimm","peter","john");
2.collect(toList())
collect(Collectors.toList())方法是將stream里的值生成一個列表,也就是將流再轉化成為集合,是一個及早求值的操作。
關於惰性求值與及早求值,這裡簡單說明一下,這兩者最重要的區別就在於看操作有沒有具體的返回值(或者說產生了具體的數值),比如上文的的統計來自英國藝術家人數的代碼,第二行代碼的操作是首先篩選出來自英國的藝術家,這個操作並沒有實際的數值產生,因此這個操作就是惰性求值,而最後的count計數方法,產生了實際的數值,因此是及早求值。惰性求值是用於描述stream流的,因此返回值是stream,而幾乎所有對於流的鏈式操作都是進行各種惰性求值的鏈式操作,最後加上一個及早求值的方法返回想要的結果。
你可以用建造者的設計模式去理解他,建造者模式通過一系列的操作進行設置與配置操作,最後調用一個build方法,創建出相應的對象。對於這裡也是同樣,調用各種惰性求值的方法,返回一個stream流,最後一步調用一個及早求值的方法,得到最終的結果。
那麼現在對於這個collect(toList()),使用方法就十分明瞭了。list.stream()//將集合轉化成流 .???()//一系列惰性求值的操作,返回值為stream .collect(toList())//及早求值,這個及早求值的方法返回值為集合,再將流轉化為集合
3. 篩選filter
你如果有耐心,看到了這裡對於這個操作應該不陌生了,filter函數接收一個Lambda表達式作為參數,該表達式返回boolean,在執行過程中,流將元素逐一輸送給filter,並篩選出執行結果為true的元素。
還是上文的例子:篩選出來自英國的藝術家
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))//惰性求值篩選
.count();//及早求值統計
4.去重distinc
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.distinct()
.count();
這樣只增加了一行,便達到了篩選出所有來自英國的藝術家,並且去掉重覆的名字之後的統計數量的目的
你看,符合了上文所說的,簡單,易懂,可讀性強。
相信下麵我說的幾個方法你一看就懂。
5.截取limit
截取流的前N個元素
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.limit(N)
.count();
6. 跳過skip
跳過流的前N個元素:
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.skip(N)
.count();
7. 映射map
如果有一個函數可以將一種類型的值轉換成另外一種類型,map操作就可以使用該函數,將一個流中的值轉換成一個新的流。
映射這個操作其實在大家編程的過程中都經常用到,也就是將A映射成B A->B
還是用藝術家的例子,現在要獲得一個包含所有來自倫敦藝術家的名字的集合
List<String> artistNames = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.map(artist -> artist.getArtistName())//將藝術家集合映射成了包含藝術家名字的集合
.collect(Collects.toList());
請註意,這裡的傳遞的Lambda表達式必須是Function介面的一個實例,Function介面是只包含一個參數的普通函數介面。
8. flatMap
上一條已經介紹過map操作,它可以用一個新的值代替stream里的值,但有時候,用戶希望讓map操作有點變化,生成一個新的steram對象取而代之,用戶通常不希望結果是一連串的流,此時flatMap能夠派上用場。
通俗的一點的說法是,他可以將一條一條的小流,匯聚成一條大流,好比海納百川的感覺。
用一個簡單的例子就很容易理解了
假設有一個包含多個集合的流,現在希望得到所有數字的序列,利用flatMap解決辦法如下
List <Integer> together = Stream.of(asList(1,2),asList(3,4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
together.forEach(n -> System.out.println(n));
輸出結果為1,2,3,4
你看,2條小流被整合成了一條流!(這就是為什麼這個類庫叫做stream,流的意思,十分的形象化)
steram流,在java8里,你可以理解成流水線,流水線的上的商品就是集合里一個個的元素,而這些對於流的各種各樣的流操作,就是流水線上加工這些商品的機器。所以呢,stream流的相關特性與之也符合
- 不可逆,無論是河流,水流,還是流水線,沒聽過有倒流的,因此java8中的流也同樣如此,你不可能在操作完第一個元素之後回頭再重新操作,這在流操作里是無法完成的。
- 另一個特性就是內部迭代,這在一開始已經講述過了。
為什麼到這裡我才做不可逆的特性說明呢,因為我覺得flatMap很能符合流的特點,水流嘛,海納百川,不可逆流,你看,完美符合java8的流特性。
9. max和min
例子:獲得所有藝術家中,年齡最大的藝術家
想一想,採用原始的外部迭代,要達到這麼簡單的要求是不是忽然感覺很麻煩?排個序?還是寫一個交替又或者是選擇比較的演算法?何必這麼麻煩!使用流操作採用內部迭代就好了,這不是我們程式猿應該專門寫一段外部程式來解決的問題!
Stream上常用的操作之一是求最大值和最小值,事實上通過流操作來完成最大值最小值的方式有很多很多種,這裡介紹的max和min的方法是stream類里的直接附帶的方法,事實上在實際操作的時候我並不會選擇這種操作方式(關於這點,在後面的章節會提到,這裡提前做一個記號,以後增加超鏈接過去)
使用流操作如下:
Artist theMaxAgeArtist = allArtists.stream()
.max(Comparator.comparing(artist -> artist.getAge()))
.get();
我們一行一行地說
- 第一行,轉化為流對象,讀到這裡的你相信已經十分理解了,因此以後對於這一行不再說明瞭
- 第二行,查找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中的流。
下一篇,將介紹整合流操作與高階流操作,我們下篇見!