讀古今文學網 > Java 8實戰 > 第4章 引入流 >

第4章 引入流

本章內容

  • 什麼是流

  • 集合與流

  • 內部迭代與外部迭代

  • 中間操作與終端操作

集合是Java中使用最多的API。要是沒有集合,還能做什麼呢?幾乎每個Java應用程序都會製造和處理集合。集合對於很多編程任務來說都是非常基本的:它們可以讓你把數據分組並加以處理。為了解釋集合是怎麼工作的,想像一下你準備列出一系列菜,組成一張菜單,然後再遍歷一遍,把每盤菜的熱量加起來。你可能想選出那些熱量比較低的菜,組成一張健康的特殊菜單。儘管集合對於幾乎任何一個Java應用都是不可或缺的,但集合操作卻遠遠算不上完美。

  • 很多業務邏輯都涉及類似於數據庫的操作,比如對幾道菜按照類別進行分組(比如全素菜餚),或查找出最貴的菜。你自己用迭代器重新實現過這些操作多少遍?大部分數據庫都允許你聲明式地指定這些操作。比如,以下SQL查詢語句就可以選出熱量較低的菜餚名稱:SELECT name FROM dishes WHERE calorie < 400。你看,你不需要實現如何根據菜餚的屬性進行篩選(比如利用迭代器和累加器),你只需要表達你想要什麼。這個基本的思路意味著,你用不著擔心怎麼去顯式地實現這些查詢語句——都替你辦好了!怎麼到了集合這裡就不能這樣了呢?

  • 要是要處理大量元素又該怎麼辦呢?為了提高性能,你需要並行處理,並利用多核架構。但寫並行代碼比用迭代器還要複雜,而且調試起來也夠受的!

那Java語言的設計者能做些什麼,來幫助你節約寶貴的時間,讓你這個程序員活得輕鬆一點兒呢?你可能已經猜到了,答案就是流。

4.1 流是什麼

流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷數據集的高級迭代器。此外,流還可以透明地並行處理,你無需寫任何多線程代碼了!我們會在第7章中詳細解釋流和並行化是怎麼工作的。我們簡單看看使用流的好處吧。下面兩段代碼都是用來返回低熱量的菜餚名稱的,並按照卡路里排序,一個是用Java 7寫的,另一個是用Java 8的流寫的。比較一下。不用太擔心Java 8代碼怎麼寫,我們在接下來的幾節裡會詳細解釋。

之前(Java 7):

List<Dish> lowCaloricDishes = new ArrayList<>;
for(Dish d: menu){
    if(d.getCalories < 400){    ←─用累加器篩選元素
        lowCaloricDishes.add(d);
    }
}
Collections.sort(lowCaloricDishes, new Comparator<Dish> {    ←─用匿名類對菜餚排序
    public int compare(Dish d1, Dish d2){
        return Integer.compare(d1.getCalories, d2.getCalories);
    }
});
List<String> lowCaloricDishesName = new ArrayList<>;
for(Dish d: lowCaloricDishes){
    lowCaloricDishesName.add(d.getName);    ←─處理排序後的菜名列表
}

  

在這段代碼中,你用了一個“垃圾變量”lowCaloricDishes。它唯一的作用就是作為一次性的中間容器。在Java 8中,實現的細節被放在它本該歸屬的庫裡了。

之後(Java 8):

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
               menu.stream
                   .filter(d -> d.getCalories < 400)    ←─選出400卡路里以下的菜餚
                   .sorted(comparing(Dish::getCalories))    ←─按照卡路里排序
                   .map(Dish::getName)    ←─提取菜餚的名稱
                   .collect(toList);    ←─將所有名稱保存在List中

  

為了利用多核架構並行執行這段代碼,你只需要把stream換成parallelStream

List<String> lowCaloricDishesName =
               menu.parallelStream
                   .filter(d -> d.getCalories < 400)
                   .sorted(comparing(Dishes::getCalories))
                   .map(Dish::getName)
                   .collect(toList);

  

你可能會想,在調用parallelStream方法的時候到底發生了什麼。用了多少個線程?對性能有多大提升?第7章會詳細討論這些問題。現在,你可以看出,從軟件工程師的角度來看,新的方法有幾個顯而易見的好處。

  • 代碼是以聲明性方式寫的:說明想要完成什麼(篩選熱量低的菜餚)而不是說明如何實現一個操作(利用循環和if條件等控制流語句)。你在前面的章節中也看到了,這種方法加上行為參數化讓你可以輕鬆應對變化的需求:你很容易再創建一個代碼版本,利用Lambda表達式來篩選高卡路里的菜餚,而用不著去複製粘貼代碼。

  • 你可以把幾個基礎操作鏈接起來,來表達複雜的數據處理流水線(在filter後面接上sortedmapcollect操作,如圖4-1所示),同時保持代碼清晰可讀。filter的結果被傳給了sorted方法,再傳給map方法,最後傳給collect方法。

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

圖 4-1 將流操作鏈接起來構成流的流水線

新的Stream API表達能力非常強。比如在讀完本章以及第5章、第6章之後,你就可以寫出像下面這樣的代碼:

Map<Dish.Type, List<Dish>> dishesByType =
    menu.stream.collect(groupingBy(Dish::getType));

  

我們在第6章中解釋這個例子。簡單來說就是,按照Map裡面的類別對菜餚進行分組。比如,Map可能包含下列結果:

{FISH=[prawns, salmon],
 OTHER=[french fries, rice, season fruit, pizza],
 MEAT=[pork, beef, chicken]}

  

想想要是改用循環這種典型的指令型編程方式該怎麼實現吧。別浪費太多時間了。擁抱這一章和接下來幾章中強大的流吧!

其他庫:Guava、Apache和lambdaj

為了給Java程序員提供更好的庫操作集合,前人已經做過了很多嘗試。比如,Guava就是谷歌創建的一個很流行的庫。它提供了multimapsmultisets等額外的容器類。Apache Commons Collections庫也提供了類似的功能。最後,本書作者Mario Fusco編寫的lambdaj受到函數式編程的啟發,也提供了很多聲明性操作集合的工具。

如今Java 8自帶了官方庫,可以以更加聲明性的方式操作集合了。

總結一下,Java 8中的Stream API可以讓你寫出這樣的代碼:

  • 聲明性——更簡潔,更易讀

  • 可復合——更靈活

  • 可並行——性能更好

在本章剩下的部分和下一章中,我們會使用這樣一個例子:一個menu,它只是一張菜餚列表。

List<Dish> menu = Arrays.asList(
    new Dish("pork", false, 800, Dish.Type.MEAT),
    new Dish("beef", false, 700, Dish.Type.MEAT),
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER),
    new Dish("rice", true, 350, Dish.Type.OTHER),
    new Dish("season fruit", true, 120, Dish.Type.OTHER),
    new Dish("pizza", true, 550, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("salmon", false, 450, Dish.Type.FISH) );

  

Dish類的定義是:

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName {
        return name;
    }

    public boolean isVegetarian {
        return vegetarian;
    }

    public int getCalories {
        return calories;
    }

    public Type getType {
        return type;
    }

    @Override
    public String toString {
        return name;
    }

    public enum Type { MEAT, FISH, OTHER }
}

  

現在就來仔細探討一下怎麼使用Stream API。我們會用流與集合做類比,做點兒鋪墊。下一章會詳細討論可以用來表達複雜數據處理查詢的流操作。我們會談到很多模式,如篩選、切片、查找、匹配、映射和歸約,還會提供很多測驗和練習來加深你的理解。

接下來,我們會討論如何創建和操縱數字流,比如生成一個偶數流,或是勾股數流。最後,我們會討論如何從不同的源(比如文件)創建流。還會討論如何生成一個具有無窮多元素的流——這用集合肯定是搞不定了!

4.2 流簡介

要討論流,我們先來談談集合,這是最容易上手的方式了。Java 8中的集合支持一個新的stream方法,它會返回一個流(接口定義在java.util.stream.Stream裡)。你在後面會看到,還有很多其他的方法可以得到流,比如利用數值範圍或從I/O資源生成流元素。

那麼,流到底是什麼呢?簡短的定義就是“從支持數據處理操作的源生成的元素序列”。讓我們一步步剖析這個定義。

  • 元素序列——就像集合一樣,流也提供了一個接口,可以訪問特定元素類型的一組有序值。因為集合是數據結構,所以它的主要目的是以特定的時間/空間複雜度存儲和訪問元素(如ArrayListLinkedList)。但流的目的在於表達計算,比如你前面見到的filtersortedmap。集合講的是數據,流講的是計算。我們會在後面幾節中詳細解釋這個思想。

  • 源——流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。 請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。

  • 數據處理操作——流的數據處理功能支持類似於數據庫的操作,以及函數式編程語言中的常用操作,如filtermapreducefindmatchsort等。流操作可以順序執行,也可並行執行。

此外,流操作有兩個重要的特點。

  • 流水線——很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線。這讓我們下一章中的一些優化成為可能,如延遲和短路。流水線的操作可以看作對數據源進行數據庫式查詢。

  • 內部迭代——與使用迭代器顯式迭代的集合不同,流的迭代操作是在背後進行的。我們在第1章中簡要地提到了這個思想,下一節會再談到它。

讓我們來看一段能夠體現所有這些概念的代碼:

import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
  menu.stream                                   ←─從menu獲得流(菜餚列表)
      .filter(d -> d.getCalories > 300)             ←─建立操作流水線:首先選出高熱量的菜餚
              .map(Dish::getName)                    ←─獲取菜名
              .limit(3)                           ←─只選擇頭三個
              .collect(toList);                   ←─將結果保存在另一個List中
        System.out.println(threeHighCaloricDishNames);    ←─結果是[pork, beef,chicken]

  

在本例中,我們先是對menu調用stream方法,由菜單得到一個流。數據源是菜餚列表(菜單),它給流提供一個元素序列。接下來,對流應用一系列數據處理操作:filtermaplimitcollect。除了collect之外,所有這些操作都會返回另一個流,這樣它們就可以接成一條流水線,於是就可以看作對源的一個查詢。最後,collect操作開始處理流水線,並返回結果(它和別的操作不一樣,因為它返回的不是流,在這裡是一個List)。在調用collect之前,沒有任何結果產生,實際上根本就沒有從menu裡選擇元素。你可以這麼理解:鏈中的方法調用都在排隊等待,直到調用collect。圖4-2顯示了流操作的順序:filtermaplimitcollect,每個操作簡介如下。

圖 4-2 使用流來篩選菜單,找出三個高熱量菜餚的名字

  • filter——接受Lambda,從流中排除某些元素。在本例中,通過傳遞lambda d -> d.getCalories > 300,選擇出熱量超過300卡路里的菜餚。

  • map——接受一個Lambda,將元素轉換成其他形式或提取信息。在本例中,通過傳遞方法引用Dish::getName,相當於Lambda d -> d.getName,提取了每道菜的菜名。

  • limit——截斷流,使其元素不超過給定數量。

  • collect——將流轉換為其他形式。在本例中,流被轉換為一個列表。它看起來有點兒像變魔術,我們在第6章中會詳細解釋collect的工作原理。現在,你可以把collect看作能夠接受各種方案作為參數,並將流中的元素累積成為一個匯總結果的操作。這裡的toList就是將流轉換為列表的方案。

注意看,我們剛剛解釋的這段代碼,與逐項處理菜單列表的代碼有很大不同。首先,我們使用了聲明性的方式來處理菜單數據,即你說的對這些數據需要做什麼:“查找熱量最高的三道菜的菜名。”你並沒有去實現篩選(filter)、提取(map)或截斷(limit)功能,Streams庫已經自帶了。因此,Stream API在決定如何優化這條流水線時更為靈活。例如,篩選、提取和截斷操作可以一次進行,並在找到這三道菜後立即停止。我們會在下一章介紹一個能體現這一點的例子。

在進一步介紹能對流做什麼操作之前,先讓我們回過頭來看看Collection API和新的Stream API的思想有何不同。

4.3 流與集合

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

我們先來打個直觀的比方吧。比如說存在DVD裡的電影,這就是一個集合(也許是字節,也許是幀,這個無所謂),因為它包含了整個數據結構。現在再來想想在互聯網上通過視頻流看同樣的電影。現在這是一個流(字節流或幀流)。流媒體視頻播放器只要提前下載用戶觀看位置的那幾幀就可以了,這樣不用等到流中大部分值計算出來,你就可以顯示流的開始部分了(想想觀看直播足球賽)。特別要注意,視頻播放器可能沒有將整個流作為集合,保存所需要的內存緩衝區——而且要是非得等到最後一幀出現才能開始看,那等待的時間就太長了。出於實現的考慮,你也可以讓視頻播放器把流的一部分緩存在集合裡,但和概念上的差異不是一回事。

粗略地說,集合與流之間的差異就在於什麼時候進行計算。集合是一個內存中的數據結構,它包含數據結構中目前所有的值——集合中的每個元素都得先算出來才能添加到集合中。(你可以往集合裡加東西或者刪東西,但是不管什麼時候,集合中的每個元素都是放在內存裡的,元素都得先算出來才能成為集合的一部分。)

相比之下,流則是在概念上固定的數據結構(你不能添加或刪除元素),其元素則是按需計算的。 這對編程有很大的好處。在第6章中,我們將展示構建一個質數流(2, 3, 5, 7, 11, …)有多簡單,儘管質數有無窮多個。這個思想就是用戶僅僅從流中提取需要的值,而這些值——在用戶看不見的地方——只會按需生成。這是一種生產者-消費者的關係。從另一個角度來說,流就像是一個延遲創建的集合:只有在消費者要求的時候才會計算值(用管理學的話說這就是需求驅動,甚至是實時製造)。

與此相反,集合則是急切創建的(供應商驅動:先把倉庫裝滿,再開始賣,就像那些曇花一現的聖誕新玩意兒一樣)。以質數為例,要是想創建一個包含所有質數的集合,那這個程序算起來就沒完沒了了,因為總有新的質數要算,然後把它加到集合裡面。當然這個集合是永遠也創建不完的,消費者這輩子都見不著了。

圖4-3用DVD對比在線流媒體的例子展示了流和集合之間的差異。

圖 4-3 流與集合

另一個例子是用瀏覽器進行互聯網搜索。假設你搜索的短語在Google或是網店裡面有很多匹配項。你用不著等到所有結果和照片的集合下載完,而是得到一個流,裡面有最好的10個或20個匹配項,還有一個按鈕來查看下面10個或20個。當你作為消費者點擊“下面10個”的時候,供應商就按需計算這些結果,然後再送回你的瀏覽器上顯示。

4.3.1 只能遍歷一次

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

List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream;
s.forEach(System.out::println);    ←─打印標題中的每個單詞
s.forEach(System.out::println);    ←─java.lang.IllegalStateException:流已被操作或關閉

  

所以要記得,流只能消費一次!

哲學中的流和集合

對於喜歡哲學的讀者,你可以把流看作在時間中分佈的一組值。相反,集合則是空間(這裡就是計算機內存)中分佈的一組值,在一個時間點上全體存在——你可以使用迭代器來訪問for-each循環中的內部成員。

集合和流的另一個關鍵區別在於它們遍歷數據的方式。

4.3.2 外部迭代與內部迭代

使用Collection接口需要用戶去做迭代(比如用for-each),這稱為外部迭代。 相反,Streams庫使用內部迭代——它幫你把迭代做了,還把得到的流值存在了某個地方,你只要給出一個函數說要幹什麼就可以了。下面的代碼列表說明了這種區別。

代碼清單4-1 集合:用for-each循環外部迭代

List<String> names = new ArrayList<>;
for(Dish d: menu){                   ←─顯式順序迭代菜單列表
    names.add(d.getName);    ←─提取名稱並將其添加到累加器
}

  

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

代碼清單4-2 集合:用背後的迭代器做外部迭代

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

  

代碼清單4-3 流:內部迭代

List<String> names = menu.stream
                         .map(Dish::getName)    ←─用getName 方法參數化map,提取菜名
                         .collect(toList);    ←─開始執行操作流水線;沒有迭代!

  

讓我們用一個比喻來解釋內部迭代的差異和好處吧。比方說你在和你兩歲的女兒索菲亞說話,希望她能把玩具收起來。

你:“索菲亞,我們把玩具收起來吧。地上還有玩具嗎?”

索菲亞:“有,球。”

你:“好,把球放進盒子裡。還有嗎?”

索菲亞:“有,那是我的娃娃。”

你:“好,把娃娃放進盒子裡。還有嗎?”

索菲亞:“有,有我的書。”

你:“好,把書放進盒子裡。還有嗎?”

索菲亞:“沒了,沒有了。”

你:“好,我們收好啦。”

這正是你每天都要對Java集合做的。你外部迭代一個集合,顯式地取出每個項目再加以處理。如果你只需跟索菲亞說“把地上所有的玩具都放進盒子裡”就好了。內部迭代比較好的原因有二:第一,索非亞可以選擇一隻手拿娃娃,另一隻手拿球;第二,她可以決定先拿離盒子最近的那個東西,然後再拿別的。同樣的道理,內部迭代時,項目可以透明地並行處理,或者用更優化的順序進行處理。要是用Java過去的那種外部迭代方法,這些優化都是很困難的。這似乎有點兒雞蛋裡挑骨頭,但這差不多就是Java 8引入流的理由了——Streams庫的內部迭代可以自動選擇一種適合你硬件的數據表示和並行實現。與此相反,一旦通過寫for-each而選擇了外部迭代,那你基本上就要自己管理所有的並行問題了(自己管理實際上意味著“某個良辰吉日我們會把它並行化”或“開始了關於任務和synchronized的漫長而艱苦的鬥爭”)。Java 8需要一個類似於Collection卻沒有迭代器的接口,於是就有了Stream!圖4-4說明了流(內部迭代)與集合(外部迭代)之間的差異。

圖 4-4 內部迭代與外部迭代

我們已經說過了集合與流在概念上的差異,特別是流利用了內部迭代:替你把迭代做了。但是,只有你已經預先定義好了能夠隱藏迭代的操作列表,例如filtermap,這個才有用。大多數這類操作都接受Lambda表達式作為參數,因此你可以用前面幾章中介紹的方法來參數化其行為。Java語言的設計者給Stream API配上了一大套可以用來表達複雜數據處理查詢的操作。我們現在先簡要地看一下這些操作,下一章中會配上例子詳細討論。

4.4 流操作

java.util.stream.Stream中的Stream接口定義了許多操作。它們可以分為兩大類。我們再來看一下前面的例子:

List<String> names = menu.stream    ←─從菜單獲得流
                         .filter(d -> d.getCalories > 300)    ←─中間操作
                         .map(Dish::getName)    ←─中間操作
                         .limit(3)    ←─中間操作
                         .collect(toList);    ←─將Stream轉換為List

  

你可以看到兩類操作:

  • filtermaplimit可以連成一條流水線;

  • collect觸發流水線執行並關閉它。

可以連接起來的流操作稱為中間操作,關閉流的操作稱為終端操作。 圖4-5中展示了這兩類操作。這種區分有什麼意義呢?

圖 4-5 中間操作與終端操作

4.4.1 中間操作

諸如filtersorted等中間操作會返回另一個流。這讓多個操作可以連接起來形成一個查詢。重要的是,除非流水線上觸發一個終端操作,否則中間操作不會執行任何處理——它們很懶。這是因為中間操作一般都可以合併起來,在終端操作時一次性全部處理。

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

List<String> names =
    menu.stream
        .filter(d -> {
                         System.out.println("filtering" + d.getName);
                         return d.getCalories > 300;
                     })                ←─打印當前篩選的菜餚
        .map(d -> {
                      System.out.println("mapping" + d.getName);
                      return d.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操作和一種稱為短路的技巧,我們會在下一章中解釋。第二,儘管filtermap是兩個獨立的操作,但它們合併到同一次遍歷中了(我們把這種技術叫作循環合併)。

4.4.2 終端操作

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

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

  

為了檢驗你對中間操作和終端操作的理解程度,試試測驗4.1吧。

測驗4.1:中間操作與終端操作

在下列流水線中,你能找出中間操作和終端操作嗎?

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

  

答案:流水線中最後一個操作count返回一個long,這是一個非Stream的值。因此它是一個終端操作。所有前面的操作,filterdistinctlimit,都是連接起來的,並返回一個Stream,因此它們是中間操作。

4.4.3 使用流

總而言之,流的使用一般包括三件事:

  • 一個數據源(如集合)來執行一個查詢;

  • 一個中間操作鏈,形成一條流的流水線;

  • 一個終端操作,執行流水線,並能生成結果。

流的流水線背後的理念類似於構建器模式。1在構建器模式中有一個調用鏈用來設置一套配置(對流來說這就是一個中間操作鏈),接著是調用built方法(對流來說就是終端操作)。

1見http://en.wikipedia.org/wiki/Builder_pattern。

為方便起見,表4-1和表4-2總結了你前面在代碼例子中看到的中間流操作和終端流操作。請注意這並不能涵蓋Stream API提供的操作,你在下一章中還會看到更多。

表4-1 中間操作

操作

類型

返回類型

操作參數

函數描述符

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>

表4-2 終端操作

操作

類型

目的

forEach

終端

消費流中的每個元素並對其應用Lambda。這一操作返回void

count

終端

返回流中元素的個數。這一操作返回long

collect

終端

把流歸約成一個集合,比如ListMap甚至是Integer。詳見第6章

在下一章中,我們會用案例詳細介紹一些可以用的流操作,讓你瞭解可以用它們表達什麼樣的查詢。我們會看到很多模式,比如過濾、切片、查找、匹配、映射和歸約,它們可以用來表達複雜的數據處理查詢。

因為第6章會非常詳細地討論收集器,所以本章和下一章僅介紹把collect終端操作用於collect(toList)的特殊情況。這一操作會創建一個與流具有相同元素的列表。

4.5 小結

以下是你應從本章中學到的一些關鍵概念。

  • 流是“從支持數據處理操作的源生成的一系列元素”。

  • 流利用內部迭代:迭代通過filtermapsorted等操作被抽像掉了。

  • 流操作有兩類:中間操作和終端操作。

  • filtermap等中間操作會返回一個流,並可以鏈接在一起。可以用它們來設置一條流水線,但並不會生成任何結果。

  • forEachcount等終端操作會返回一個非流的值,並處理流水線以返回結果。

  • 流中的元素是按需計算的。