談一談Java8的函數式編程(二) –Java8中的流(轉)

談一談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()//進行流操作
                           .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對象,這個對象對我們來確實有點陌生,第11條我會專門對這個對象進行介紹,在這裏需要記住的是,通過get方法可以取得Optional對象中的值。

10.歸約reduce

reduce操作可以實現從一組值中生成一個值。在上述例子中用到的count,min,max方法,因爲經常使用,所以被納入了標準庫裏,實際上,這些方法都是由reduce操作實現的。
reduce函數接收兩個參數:

  • 初始值
  • 進行歸約操作的Lambda表達式

舉個例子 使用reduce進行求和

    int count = Stream.of(1,2,3)
                      .reduce(0,(acc,element) -> acc + element);

reduce的第一個參數表示初始值爲0;
reduce的第二個參數爲需要進行的歸約操作,它接收一個擁有兩個參數的Lambda表達式,以上代碼acc參數代表當前的數值總和,element代表下一個元素,reduce會把流中的元素兩兩輸給Lambda表達式,最後將計算出累加之和。
也就是說每次acc+element的返回值都會賦給acc
在上述求和例子中,計算過程是這樣的 初始值爲0

  • 0 + 1 = 1
  • 1 + 2 = 3
  • 3 + 3 = 6
    以上三行就是 acc + elment = acc ,其中acc的初始值爲reduce的第一個參數(在本例中初始值爲0)

上面的方法中我們自己定義了Lambda表達式實現求和運算,如果當前流的元素爲數值類型,那麼可以使用Integer提供了sum函數代替自定義的Lambda表達式,如:

int age = list.stream().reduce(0, Integer::sum);

Integer類還提供了min、max等一系列數值操作,當流中元素爲數值類型時可以直接使用。

注: 上面的Integer::sum如果不理解的話,這是java8中引用的方法,是一種簡寫語法,屬於語法糖。
一般格式爲類名(或者是類的實例對象) :: 方法名(注意這裏只是方法名,沒有括號),這裏引用了Integer裏的sum函數(java8裏新增的),下面是Integer裏的sum函數源碼

    /**
     * Adds two integers together as per the + operator.
     *
     * @param a the first operand
     * @param b the second operand
     * @return the sum of {@code a} and {@code b}
     * @see java.util.function.BinaryOperator
     * @since 1.8
     */
    public static int sum(int a, int b) {
        return a + b;
    }

11. Optional對象

Optional是Java8新加入的一個容器,這個容器只存1個或0個元素,它用於防止出現NullpointException,它提供如下方法:

  • isPresent()
    判斷容器中是否有值。
  • ifPresent(Consume lambda)
    容器若不爲空則執行括號中的Lambda表達式。
  • T get()
    獲取容器中的元素,若容器爲空則拋出NoSuchElement異常。
  • T orElse(T other)
    獲取容器中的元素,若容器爲空則返回括號中的默認值。

值得注意的是,Optional對象不僅可以用於新的Java 8 API,也可用於具體領域類中,和普通的類並沒有什麼區別,當試圖避免空值相關的缺陷,如捕獲的異常時,可以考慮一下是否可使用Optional對象。

本篇小結

  本篇以一個藝術家的例子介紹了流與基本流的相關操作,目的是爲了讓看到本篇博客的人嘗試着使用這樣的函數式方法,並開始理解什麼是java8中的流。
  下一篇是關於本篇流操作的幾道練習題,方便大家鞏固
  談一談Java8的函數式編程(三) –幾道關於流的練習題



如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!歡迎各位轉載,但是未經作者本人同意,轉載文章之後必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利。
« 上一篇:談一談Java8的函數式編程 (一)
» 下一篇:談一談Java8的函數式編程 (三) –幾道關於流的練習題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章