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

来源:http://www.cnblogs.com/invoker-/archive/2017/05/24/6896865.html
-Advertisement-
Play Games

流與集合    眾所周知,日常開發與操作中涉及到集合的操作相當頻繁,而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中的流。
  下一篇,將介紹整合流操作與高階流操作,我們下篇見!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java把記憶體分成兩種,一種叫做棧記憶體,一種叫做堆記憶體 在函數中定義的一些基本類型的變數和對象的引用變數都是在函數的棧記憶體中分配。當在一段代碼塊中定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當超過變數的作用域後,java會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立刻被另作他用。 ...
  • 什麼是框架? 是軟體的半成品,它已經完成了部分功能。 什麼是hibernate? hibernate是一個開放源代碼的對象關係映射(ORM)框架,它對JDBC進行了非常輕量級的對象封裝,使得Java程式員可以隨心所欲的使用對象編程思維來操縱資料庫。 (簡單的說,hibernate框架通過映射(xml ...
  • Java基礎八 一、子父類中構造函數的特點 1.1 為什麼在子類構造對象時,發現,訪問子類構造函數時,父類也運行了呢? 原因是:在子類的構造函數中第一行有一個預設的隱式語句。 super(); 構造方法中其實還有一句return; 子類的實例化過程:子類中所有的構造函數預設都會訪問父類中的空參數的構 ...
  • Java爬蟲 一、代碼 爬蟲的實質就是打開網頁源代碼進行匹配查找,然後獲取查找到的結果。 打開網頁: URL url = new URL("http://www.cnblogs.com/Renyi-Fan/p/6896901.html"); 讀取網頁內容: BufferedReader bufr = ...
  • 轉載請出自出處:http://www.cnblogs.com/hd3013779515/ 1.集群容錯和負載均衡原理 各節點關係: 這裡的Invoker是Provider的一個可調用Service的抽象,Invoker封裝了Provider地址及Service介面信息。 Directory代表多個I ...
  • Java基礎七-正則表達式 一、定義: 特定的符號的組合 二、作用: 用於操作字元串數據 三、優缺點 簡化代碼,但是閱讀性差 四、引入 4.1 問題 判斷一個號碼是否是QQ號? 不是零開頭 6-15位 只含有數字 4.2 代碼 4.3 要點 if(!qq.startsWith("0")) 判斷不是零 ...
  • 聲明:我使用的elasticsearch的版本是5.4.0,具體參考下麵的鏈接 https://www.elastic.co/guide/en/elasticsearch/reference/5.4/query-dsl-filtered-query.html filtered 查詢已經被bool 查 ...
  • 題目 "找出直系親屬" 代碼和分析 import java.util.Arrays; import java.util.Scanner; / 題意上,類似樹,這裡左節點和右節點是根節點的雙親。 用了並查集。 這裡並查集的parent向量,記錄的仍是該點所在樹的父節點。 / public class ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...