《Java8實戰》-第四章讀書筆記(引入流Stream)

流(Stream)

流是什麼

流是Java API的新成員,它容許你以聲明性方式處理數據集合(經過查詢語句來表達,而不是臨時編寫一個實現)。就如今來講,你能夠把它們當作遍歷數據集的高級迭代器。此外,流還能夠透明地並行處理,你無需寫任何多線程代碼了!我會在後面的筆記中詳細記錄和解釋流和並行化是怎麼工做的。咱們簡單看看使用流的好處吧。下面兩段代碼都是用來返回低熱量的菜餚名稱的,並按照卡路里排序,一個是用Java7寫的,另外一個是用Java8的流寫的。比較一下。不用太擔憂Java 8代碼怎麼寫,咱們在接下來會對它進行詳細的瞭解。java

菜單篩選

使用Java7:git

private static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
    List<Dish> lowCaloricDishes = new ArrayList<>();
    // 遍歷篩選出低於400卡路里的菜,添加到另一個集合中
    for (Dish d : dishes) {
        if (d.getCalories() < 400) {
            lowCaloricDishes.add(d);
        }
    }

    // 對集合按照卡路里大小進行排序
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
        @Override
        public int compare(Dish d1, Dish d2) {
            return Integer.compare(d1.getCalories(), d2.getCalories());
        }
    });

    // 遍歷將菜名添加到另一個集合中
    for (Dish d : lowCaloricDishes) {
        lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
}

在上面的代碼中,看起來很冗長,咱們使用了一個「垃圾變量」lowCaloricDishes。它惟一的做用就是做爲一次性的中間容器。 在Java8,實現的細節被放到了它本該歸屬的庫力了。
使用Java8:github

private static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes.stream()
            // 選出400卡路里如下的菜餚
            .filter(d -> d.getCalories() < 400)
            // 按照卡路里排序
            .sorted(comparing(Dish::getCalories))
            // 提取菜名
            .map(Dish::getName)
            // 轉爲集合
            .collect(toList());
}

太酷了!本來十幾行的代碼,如今只須要一行就能夠搞定,這樣的感受真的是太棒了!還有一個很棒的新特性,爲了利用多核架構並行執行代碼,咱們只須要將stream()改成parallelStream()便可:數據庫

private static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes
            .parallelStream()
            // 選出400卡路里如下的菜餚
            .filter(d -> d.getCalories() < 400)
            // 按照卡路里排序
            .sorted(comparing(Dish::getCalories))
            // 提取菜名
            .map(Dish::getName)
            // 轉爲集合
            .collect(toList());
}

你可能會想,在調用parallelStream方法時到底發生了什麼。用了多少個線程?對性能有多大的提高?不用着急,在後面的讀書筆記中會討論這些問題。如今,你能夠看出,從軟件工程師的角度來看,新的方法有幾個顯而易見的好處。編程

  1. 代碼是以聲明性的方式寫的:說明想要完成什麼(篩選熱量低的菜餚)而不是說明如何實現一個操做(利用循環和if條件等控制流語句)。
  2. 你能夠把幾個基礎操做連接起來,來表達複雜的數據處理流水線(在 filter 後面接上

sorted 、 map 和 collect 操做),同時保持代碼清晰可讀。 filter 的結果被傳給了 sorted 方法,再傳給 map 方法,最後傳給 collect 方法。數組

filter、sorted、map和collect等操做是與具體線程模型無關的高層次構件,因此它們的內部實現能夠是單線程的,也可能透明地充分利用你的多核架構!在實踐中,這意味着咱們用不着爲了讓某些數據處理任務並行而去操心線程和鎖了,Stream API都替你作好了!數據結構

如今就來仔細探討一下怎麼使用Stream API。咱們會用流與集合作類比,作點兒鋪墊。下一
章會詳細討論能夠用來表達複雜數據處理查詢的流操做。咱們會談到不少模式,如篩選、切片、
查找、匹配、映射和歸約,還會提供不少測驗和練習來加深你的理解。接下來,咱們會討論如何建立和操縱數字流,好比生成一個偶數流,或是勾股數流。最後,咱們會討論如何從不一樣的源(好比文件)建立流。還會討論如何生成一個具備無窮多元素的流,這用集合確定是搞不定。多線程

流簡介

要討論流,咱們首先來談談集合,這是最容易上手的方式了。Java8中的集合支持一個新的stream方法,它會返回一個流(接口定義在 java.util.stream.Stream 裏)。你在後面會看到,還有不少其餘的方法能夠獲得流,好比利用數值範圍或從I/O資源生成流元素。架構

那麼,流究竟是什麼呢?簡短的定義就是「從支持數據處理操做的源生成的元素序列」。讓咱們一步步剖析這個定義。app

  1. 元素序列:就像集合同樣,流也提供了一個接口,能夠訪問特定元素類型的一組有序值。由於集合是數據結構,因此它的主要目的是以特定的時間/空間複雜度存儲和訪問元素(如ArrayList 與 LinkedList )。但流的目的在於表達計算,好比你前面見到的filter 、 sorted 和 map 。集合講的是數據,流講的是計算。
  2. 源:流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。
  3. 數據處理操做:流的數據處理功能支持相似於數據庫的操做,以及函數式編程語言中的經常使用操做,如filter、map、reduce、find、match、sort等。流操做能夠順序執行,也可並行執行。

此外,流操做有兩個重要的特色。

  1. 流水線:不少流操做自己會返回一個流,這樣多個操做就能夠連接起來,造成一個大的流水線。
  2. 內部迭代:與使用迭代器顯式迭代的集合不一樣,流的迭代操做是在背後進行的。

讓咱們來看一段可以體現全部這些概念的代碼:

List<Dish> menu = Dish.MENU;
// 從menu得到流
List<String> threeHighCaloricDishNames = menu.stream()
        // 經過鏈式操做,篩選出高熱量的菜餚
        .filter(d -> d.getCalories() > 300)
        // 獲取菜名
        .map(Dish::getName)
        .limit(3)
        .collect(Collectors.toList());
// [pork, beef, chicken]
System.out.println(threeHighCaloricDishNames);

看起來很簡單,就算不明白也不要緊,咱們來了解了解,剛剛使用到的一些方法:

  1. filter: 接受Lambda,從流中排除某些元素。在剛剛的代碼中,經過傳遞Lambda表達式 d -> d.getCalories() > 300,選擇出熱量高於300卡路里的菜餚。
  2. map:接受一個Lambda,將元素轉換成其餘形式或提取信息。在剛剛的代碼中,經過傳遞方法引用Dish::getName,提取了每道菜的菜名。
  3. limit:截斷流,使其元素不超過給定的數量。
  4. collect:將流轉換爲其餘形式。在剛剛的代碼中,流被轉爲一個List集合。

在剛剛解釋的這段代碼,與遍歷處理菜單集合的代碼有很大的不一樣。首先,咱們使用了聲明性的方式來處理菜單數據。咱們並無去實現篩選(filter)、提取(map)或截斷(limit)功能,Stream庫已經自帶了。所以,StreamAPI在決定如何優化這條流水線時更爲靈活。例如,篩選、提取和截斷操做能夠一次進行,並在找到這三道菜後當即中止。

流與集合

Java現有的集合概念和新的流概念都提供了接口,來配合表明元素型有序值的數據接口。所謂有序,就是說咱們通常是按順序取用值,而不是隨機取用的。那這二者有什麼區別呢?

打個比方說,咱們在看電影的時候,這些視頻就是一個流(字節流或幀流),流媒體視頻播放器只要提早下載用戶觀看位置的那幾幀就能夠了,這樣不用等到流中大部分值計算出來。好比,咱們在Youtube上看的視頻進度條隨便拖動到一個位置,你會發現它很快就開始播放了,不須要將整個視頻都加載好,而是加載了一段。若是,不按照這種方式的話,咱們能夠想象一下,視頻播放器可能沒有將整個流做爲集合,保存所須要的內存緩衝區——並且要是非得等到最後一幀出現才能開始看,那等待的時間就太長了,早就沒耐心看了。

初略地說,集合與流之間的差別就在於何時進行計算。集合是一個內存中的數據結構,它包含數據結構中目前全部的值,集合中的每一個元素都得先算出來才能添加到集合中。

相比之下,流則是在概念上固定的數據結構,其元素則是按需計(懶加載)算的。須要多少就給多少。這是一種生產者與消費者的關係。從另外一個角度來講,流就像是一個延遲建立的集合:只有在消費者要求的時候纔會生成值。與之相反,集合則是急切建立的(就像黃牛囤貨同樣)。

流只能遍歷一次

請注意,和迭代器相似,流只能遍歷一次。遍歷完以後,咱們就說這個流已經被消費掉了。你能夠從原始數據源那裏再得到一個新的流來從新遍歷一遍,就像迭代器同樣(這裏假設它是集合之類的可重複的源,若是是I/O通道就沒戲了)。例如如下代碼會拋出一個異常,說流已被消費掉了:

List<String> names = Arrays.asList("Java8", "Lambdas", "In", "Action");
Stream<String> s = names.stream();
s.forEach(System.out::println);
// 再繼續執行一次,則會拋出異常
s.forEach(System.out::println);

千萬要記住,它只能消費一次!

外部迭代與內部迭代

使用Collection接口須要用用戶去作迭代(好比用for-each),這個稱爲外部迭代。反之,Stream庫使用內部迭代,它幫你把迭代作了,還把獲得的流值存在了某個地方,你只要給出一個函數說要幹什麼就能夠了。下面的代碼說明了這種區別。

集合:使用for-each循環外部迭代:

// 集合:使用for-each循環外部迭代
List<Dish> menu = Dish.MENU;
List<String> names = new ArrayList<>();
for (Dish dish : menu) {
    names.add(dish.getName());
}

請注意, for-each 還隱藏了迭代中的一些複雜性。for-each結構是一個語法糖,它背後的東西用Iterator對象表達出來更要醜陋得多。

集合:用背後的迭代器作外部迭代

List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish d = iterator.next();
    names.add(d.getName());
}

流:內部迭代

List<String> names = menu.stream()
                    .map(Dish::getName)
                    .collect(toList());

讓咱們用一個比喻來解釋一下內部迭代的差別和好處吧!比方說你在和你兩歲的兒子說話,但願他能把玩家收起來。

你:「兒子,咱們把玩家收起來吧。地上還有玩具嗎?」
兒子:「有,球。」
你:「好,放進盒子裏。還有嗎?」
兒子:「有,那是個人飛機。」
你:「好,放進盒子裏。還有嗎?」
兒子:「有,個人書。」
你:「好,放進盒子裏。還有嗎?」
兒子:「沒了,沒有了。」
你:「好,咱們收好啦!」

這正是你天天都要對Java集合作的。你外部迭代一個集合,顯式地取出每一個項目再加以處理。若是,你對兒子說「把地上的全部玩具都放進盒子裏收起來」就行了。內部迭代比較好的緣由有二:第一,兒子能夠選擇一隻手拿飛機,另外一隻手拿球第二,他能夠決定先拿離盒子最近的那個東西,而後再拿別的。一樣的道理,內部迭代時,項目能夠透明地並行處理,或者用更優化的順序進行處理。要是用Java過去的那種外部迭代方法,這些優化都是很困難的。這彷佛有點兒雞蛋裏挑骨頭,但這差很少就是Java 8引入流的理由了,Stream庫的內部迭代能夠自動選擇一種適合你硬件的數據表示和並行實現。與此相反,一旦經過寫 for-each 而選擇了外部迭代,那你基本上就要本身管理全部的並行問題了(本身管理實際上意味着「某個良辰吉日咱們會把它並行化」或「開始了關於任務和 synchronized 的漫長而艱苦的鬥爭」)。Java8須要一個相似於Collection 卻沒有迭代器的接口,因而就有了Stream!下面的圖說明了流(內部迭代)與集合(外部迭代)之間的差別。

image

咱們已經瞭解過了集合與流在概念上的差別,特別是利用內部迭代:替你把迭代作了。可是,只有你已經預先定義好了可以隱藏迭代的操做集合。例如filter或map,這個纔有用。大多數這類操做都接受Lambda表達式做爲參數,所以咱們能夠用前面所瞭解的知識來參數化其行爲。

流操做

java.util.stream.Stream 中的 Stream 接口定義了許多操做。它們能夠分爲兩大類。咱們再來看一下前面的例子:

List<String> names = menu.stream()
                // 中間操做
                .filter(d -> d.getCalories() > 300)
                // 中間操做
                .map(Dish::getName)
                // 中間操做
                .limit(3)
                // 將Stream轉爲List
                .collect(toList());

filter、map和limit能夠連成一條線,collect觸發流水線執行並關閉它。能夠連起來的稱爲中間操做,關閉流的操做能夠稱爲終端操做。

中間操做

諸如filter和sorted等中間操做會返回一個流。讓多個操做能夠鏈接起來造成一個查詢。重要的是,除非流水線上觸發一個終端操做,不然中間操做不會執行任何處理它們懶得很。這就是由於中間操做通常均可以合併起來,在終端操做時一次性所有處理。

爲了搞清楚流水線到底發生了什麼,咱們把代碼改一改,讓每一個Lambda都打印出當前處理的菜餚(就像不少演示和調試技巧同樣,這種編程風格要是擱在生產代碼裏那就嚇死人了,可是學習的時候卻能夠直接看清楚求值的順序):

List<String>  names = menu.stream()
        .filter(d -> {
            System.out.println("filtering:" + d.getName());
            return d.getCalories() > 300;
        })
        .map(dish -> {
            System.out.println("mapping:" + dish.getName());
            return dish.getName();
        })
        .limit(3)
        .collect(toList());
System.out.println(names);

執行結果:

filtering:pork
mapping:pork
filtering:beef
mapping:beef
filtering:chicken
mapping:chicken
[pork, beef, chicken]

從上面的打印結果,咱們能夠發現有好幾種優化利用了流的延遲性質。第一,儘管有不少熱量都高於300卡路里,可是隻會選擇前三個!由於limit操做和一種稱爲短路的技巧,第二,儘管filter和map是兩個獨立的操做,可是它們合併到同一次便利中了(咱們把這種技術叫作循環合併)。

終端操做

終端操做會從流的流水線生產結果。其結果是任何不是流的值,好比List、Integer,甚至是void。例如,在下面的流水線中,foreachh返回的是一個void的終端操做,它對源中的每道菜應用一個Lambda。把System.out.println()傳遞給foreach,並要求它打印出由menu生成的流中每個Dish:

menu.stream().forEach(System.out::println);

爲了檢驗一下對終端操做已經中間操做的理解,下面咱們一塊兒來看看一個例子:

下面哪些是中間操做哪些是終端操做?

long count = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .distinct()
            .limit(3)
            .count();

答案:流水線中最後一個操做是count,它會返回一個long,這是一個非Stream的值。所以,它是終端操做。

使用流

總而言之,流的使用通常包括三件事:

  1. 一個數據源(好比集合)來執行查詢
  2. 一箇中間操做鏈,造成一條流的流水線
  3. 一個終端操做,執行流水線,並能生成結果。

流的流水線背後的理念相似於構建器模式。 在構建器模式中有一個調用鏈用來設置一套配置(對流來講這就是一箇中間操做鏈),接着是調用built方法(對流來講就是終端操做)。其實,咱們目前所看的Stream的例子用到的方法並非它的所有,還有一些其餘的一些操做。

在本章中,咱們所接觸到的一些中間操做與終端操做:

中間:

操做 類型 返回類型 操做參數 函數描述
filter 中間 Stream<T> Predicate<T> T -> boolean
map 中間 Stream<R> Function<T, R> T -> R
limit 中間 Stream<T>
sorted 中間 Stream<T> Comparator<T> (T, T) -> int
distinct 中間 Stream<T>

終端:

操做 類型 目的
foreach 終端 消費流中的每一個元素並對其應用 Lambda。這一操做返回 void
count 終端 返回流中元素的個數。這一操做返回 long
collect 終端 把流歸約成一個集合,好比 List 、 Map 甚至是 Integer

Stream是一個很是好用的一個新特性,它能幫助咱們簡化不少冗長的代碼,提升咱們代碼的可讀性。

本章總結

  1. 流是「從支持數據處理操做的源生成的一系列元素」。
  2. 流利用內部迭代:迭代經過filter、map、sorted等操做被抽象掉了。
  3. 流操做有兩類:中間操做和終端操做。
  4. filter和map等中間操做會返回一個流,並能夠連接在一塊兒。能夠用它們來設置一條流水線,但並不會生成任何結果。
  5. forEach和count等終端操做會返回一個非流的值,並處理流水線以返回結果。

6.流中的元素是按需計算(懶加載)的。

代碼示例

Github: chap4

Gitee: chap4

公衆號

若是,你對Java8中的新特性很感興趣,你能夠關注個人公衆號或者當前的技術社區的帳號,利用空閒的時間看看個人筆記,很是感謝!

相關文章
相關標籤/搜索