摘要:內部迭代與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。流只能遍歷一次請注意,和迭代器類似,流只能遍歷一次。
流(Stream) 流是什么
流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷數據集的高級迭代器。此外,流還可以透明地并行處理,你無需寫任何多線程代碼了!我會在后面的筆記中詳細記錄和解釋流和并行化是怎么工作的。我們簡單看看使用流的好處吧。下面兩段代碼都是用來返回低熱量的菜肴名稱的,并按照卡路里排序,一個是用Java7寫的,另一個是用Java8的流寫的。比較一下。不用太擔心Java 8代碼怎么寫,我們在接下來會對它進行詳細的了解。
菜單篩選使用Java7:
private static ListgetLowCaloricDishesNamesInJava7(List dishes) { List lowCaloricDishes = new ArrayList<>(); // 遍歷篩選出低于400卡路里的菜,添加到另外一個集合中 for (Dish d : dishes) { if (d.getCalories() < 400) { lowCaloricDishes.add(d); } } // 對集合按照卡路里大小進行排序 List lowCaloricDishesName = new ArrayList<>(); Collections.sort(lowCaloricDishes, new Comparator () { @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:
private static ListgetLowCaloricDishesNamesInJava8(List dishes) { return dishes.stream() // 選出400卡路里以下的菜肴 .filter(d -> d.getCalories() < 400) // 按照卡路里排序 .sorted(comparing(Dish::getCalories)) // 提取菜名 .map(Dish::getName) // 轉為集合 .collect(toList()); }
太酷了!原本十幾行的代碼,現在只需要一行就可以搞定,這樣的感覺真的是太棒了!還有一個很棒的新特性,為了利用多核架構并行執行代碼,我們只需要將stream()改為parallelStream()即可:
private static ListgetLowCaloricDishesNamesInJava8(List dishes) { return dishes .parallelStream() // 選出400卡路里以下的菜肴 .filter(d -> d.getCalories() < 400) // 按照卡路里排序 .sorted(comparing(Dish::getCalories)) // 提取菜名 .map(Dish::getName) // 轉為集合 .collect(toList()); }
你可能會想,在調用parallelStream方法時到底發生了什么。用了多少個線程?對性能有多大的提升?不用著急,在后面的讀書筆記中會討論這些問題。現在,你可以看出,從軟件工程師的角度來看,新的方法有幾個顯而易見的好處。
代碼是以聲明性的方式寫的:說明想要完成什么(篩選熱量低的菜肴)而不是說明如何實現一個操作(利用循環和if條件等控制流語句)。
你可以把幾個基礎操作鏈接起來,來表達復雜的數據處理流水線(在 filter 后面接上
sorted 、 map 和 collect 操作),同時保持代碼清晰可讀。 filter 的結果被傳給了 sorted 方法,再傳給 map 方法,最后傳給 collect 方法。
filter、sorted、map和collect等操作是與具體線程模型無關的高層次構件,所以它們的內部實現可以是單線程的,也可能透明地充分利用你的多核架構!在實踐中,這意味著我們用不著為了讓某些數據處理任務并行而去操心線程和鎖了,Stream API都替你做好了!
現在就來仔細探討一下怎么使用Stream API。我們會用流與集合做類比,做點兒鋪墊。下一
章會詳細討論可以用來表達復雜數據處理查詢的流操作。我們會談到很多模式,如篩選、切片、
查找、匹配、映射和歸約,還會提供很多測驗和練習來加深你的理解。接下來,我們會討論如何創建和操縱數字流,比如生成一個偶數流,或是勾股數流。最后,我們會討論如何從不同的源(比如文件)創建流。還會討論如何生成一個具有無窮多元素的流,這用集合肯定是搞不定。
要討論流,我們首先來談談集合,這是最容易上手的方式了。Java8中的集合支持一個新的stream方法,它會返回一個流(接口定義在 java.util.stream.Stream 里)。你在后面會看到,還有很多其他的方法可以得到流,比如利用數值范圍或從I/O資源生成流元素。
那么,流到底是什么呢?簡短的定義就是“從支持數據處理操作的源生成的元素序列”。讓我們一步步剖析這個定義。
元素序列:就像集合一樣,流也提供了一個接口,可以訪問特定元素類型的一組有序值。因為集合是數據結構,所以它的主要目的是以特定的時間/空間復雜度存儲和訪問元素(如ArrayList 與 LinkedList )。但流的目的在于表達計算,比如你前面見到的filter 、 sorted 和 map 。集合講的是數據,流講的是計算。
源:流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。
數據處理操作:流的數據處理功能支持類似于數據庫的操作,以及函數式編程語言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以順序執行,也可并行執行。
此外,流操作有兩個重要的特點。
流水線:很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線。
內部迭代:與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。
讓我們來看一段能夠體現所有這些概念的代碼:
Listmenu = Dish.MENU; // 從menu獲得流 List threeHighCaloricDishNames = menu.stream() // 通過鏈式操作,篩選出高熱量的菜肴 .filter(d -> d.getCalories() > 300) // 獲取菜名 .map(Dish::getName) .limit(3) .collect(Collectors.toList()); // [pork, beef, chicken] System.out.println(threeHighCaloricDishNames);
看起來很簡單,就算不明白也沒關系,我們來了解了解,剛剛使用到的一些方法:
filter: 接受Lambda,從流中排除某些元素。在剛剛的代碼中,通過傳遞Lambda表達式 d -> d.getCalories() > 300,選擇出熱量高于300卡路里的菜肴。
map:接受一個Lambda,將元素轉換成其他形式或提取信息。在剛剛的代碼中,通過傳遞方法引用Dish::getName,提取了每道菜的菜名。
limit:截斷流,使其元素不超過給定的數量。
collect:將流轉換為其他形式。在剛剛的代碼中,流被轉為一個List集合。
在剛剛解釋的這段代碼,與遍歷處理菜單集合的代碼有很大的不同。首先,我們使用了聲明性的方式來處理菜單數據。我們并沒有去實現篩選(filter)、提取(map)或截斷(limit)功能,Stream庫已經自帶了。因此,StreamAPI在決定如何優化這條流水線時更為靈活。例如,篩選、提取和截斷操作可以一次進行,并在找到這三道菜后立即停止。
流與集合Java現有的集合概念和新的流概念都提供了接口,來配合代表元素型有序值的數據接口。所謂有序,就是說我們一般是按順序取用值,而不是隨機取用的。那這兩者有什么區別呢?
打個比方說,我們在看電影的時候,這些視頻就是一個流(字節流或幀流),流媒體視頻播放器只要提前下載用戶觀看位置的那幾幀就可以了,這樣不用等到流中大部分值計算出來。比如,我們在Youtube上看的視頻進度條隨便拖動到一個位置,你會發現它很快就開始播放了,不需要將整個視頻都加載好,而是加載了一段。如果,不按照這種方式的話,我們可以想象一下,視頻播放器可能沒有將整個流作為集合,保存所需要的內存緩沖區——而且要是非得等到最后一幀出現才能開始看,那等待的時間就太長了,早就沒耐心看了。
初略地說,集合與流之間的差異就在于什么時候進行計算。集合是一個內存中的數據結構,它包含數據結構中目前所有的值,集合中的每個元素都得先算出來才能添加到集合中。
相比之下,流則是在概念上固定的數據結構,其元素則是按需計(懶加載)算的。需要多少就給多少。這是一種生產者與消費者的關系。從另一個角度來說,流就像是一個延遲創建的集合:只有在消費者要求的時候才會生成值。與之相反,集合則是急切創建的(就像黃牛囤貨一樣)。
流只能遍歷一次請注意,和迭代器類似,流只能遍歷一次。遍歷完之后,我們就說這個流已經被消費掉了。你可以從原始數據源那里再獲得一個新的流來重新遍歷一遍,就像迭代器一樣(這里假設它是集合之類的可重復的源,如果是I/O通道就沒戲了)。例如以下代碼會拋出一個異常,說流已被消費掉了:
Listnames = Arrays.asList("Java8", "Lambdas", "In", "Action"); Stream s = names.stream(); s.forEach(System.out::println); // 再繼續執行一次,則會拋出異常 s.forEach(System.out::println);
千萬要記住,它只能消費一次!
外部迭代與內部迭代使用Collection接口需要用用戶去做迭代(比如用for-each),這個稱為外部迭代。反之,Stream庫使用內部迭代,它幫你把迭代做了,還把得到的流值存在了某個地方,你只要給出一個函數說要干什么就可以了。下面的代碼說明了這種區別。
集合:使用for-each循環外部迭代:
// 集合:使用for-each循環外部迭代 Listmenu = Dish.MENU; List names = new ArrayList<>(); for (Dish dish : menu) { names.add(dish.getName()); }
請注意, for-each 還隱藏了迭代中的一些復雜性。for-each結構是一個語法糖,它背后的東西用Iterator對象表達出來更要丑陋得多。
集合:用背后的迭代器做外部迭代
Listnames = new ArrayList<>(); Iterator iterator = menu.iterator(); while(iterator.hasNext()) { Dish d = iterator.next(); names.add(d.getName()); }
流:內部迭代
Listnames = menu.stream() .map(Dish::getName) .collect(toList());
讓我們用一個比喻來解釋一下內部迭代的差異和好處吧!比方說你在和你兩歲的兒子說話,希望他能把玩家收起來。
你:“兒子,我們把玩家收起來吧。地上還有玩具嗎?” 兒子:“有,球。” 你:“好,放進盒子里。還有嗎?” 兒子:“有,那是我的飛機。” 你:“好,放進盒子里。還有嗎?” 兒子:“有,我的書。” 你:“好,放進盒子里。還有嗎?” 兒子:“沒了,沒有了。” 你:“好,我們收好啦!”
這正是你每天都要對Java集合做的。你外部迭代一個集合,顯式地取出每個項目再加以處理。如果,你對兒子說“把地上的所有玩具都放進盒子里收起來”就好了。內部迭代比較好的原因有二:第一,兒子可以選擇一只手拿飛機,另一只手拿球第二,他可以決定先拿離盒子最近的那個東西,然后再拿別的。同樣的道理,內部迭代時,項目可以透明地并行處理,或者用更優化的順序進行處理。要是用Java過去的那種外部迭代方法,這些優化都是很困難的。這似乎有點兒雞蛋里挑骨頭,但這差不多就是Java 8引入流的理由了,Stream庫的內部迭代可以自動選擇一種適合你硬件的數據表示和并行實現。與此相反,一旦通過寫 for-each 而選擇了外部迭代,那你基本上就要自己管理所有的并行問題了(自己管理實際上意味著“某個良辰吉日我們會把它并行化”或“開始了關于任務和 synchronized 的漫長而艱苦的斗爭”)。Java8需要一個類似于Collection 卻沒有迭代器的接口,于是就有了Stream!下面的圖說明了流(內部迭代)與集合(外部迭代)之間的差異。
我們已經了解過了集合與流在概念上的差異,特別是利用內部迭代:替你把迭代做了。但是,只有你已經預先定義好了能夠隱藏迭代的操作集合。例如filter或map,這個才有用。大多數這類操作都接受Lambda表達式作為參數,因此我們可以用前面所了解的知識來參數化其行為。
流操作java.util.stream.Stream 中的 Stream 接口定義了許多操作。它們可以分為兩大類。我們再來看一下前面的例子:
Listnames = menu.stream() // 中間操作 .filter(d -> d.getCalories() > 300) // 中間操作 .map(Dish::getName) // 中間操作 .limit(3) // 將Stream轉為List .collect(toList());
filter、map和limit可以連成一條線,collect觸發流水線執行并關閉它。可以連起來的稱為中間操作,關閉流的操作可以稱為終端操作。
中間操作諸如filter和sorted等中間操作會返回一個流。讓多個操作可以連接起來形成一個查詢。重要的是,除非流水線上觸發一個終端操作,否則中間操作不會執行任何處理它們懶得很。這就是因為中間操作一般都可以合并起來,在終端操作時一次性全部處理。
為了搞清楚流水線到底發生了什么,我們把代碼改一改,讓每個Lambda都打印出當前處理的菜肴(就像很多演示和調試技巧一樣,這種編程風格要是擱在生產代碼里那就嚇死人了,但是學習的時候卻可以直接看清楚求值的順序):
Listnames = 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的值。因此,它是終端操作。
使用流總而言之,流的使用一般包括三件事:
一個數據源(比如集合)來執行查詢
一個中間操作鏈,形成一條流的流水線
一個終端操作,執行流水線,并能生成結果。
流的流水線背后的理念類似于構建器模式。 在構建器模式中有一個調用鏈用來設置一套配置(對流來說這就是一個中間操作鏈),接著是調用built方法(對流來說就是終端操作)。其實,我們目前所看的Stream的例子用到的方法并不是它的全部,還有一些其他的一些操作。
在本章中,我們所接觸到的一些中間操作與終端操作:
中間:
操作 | 類型 | 返回類型 | 操作參數 | 函數描述 |
---|---|---|---|---|
filter | 中間 | Stream |
Predicate |
T -> boolean |
map | 中間 | Stream |
Function |
T -> R |
limit | 中間 | Stream |
||
sorted | 中間 | Stream |
Comparator |
(T, T) -> int |
distinct | 中間 | Stream |
終端:
操作 | 類型 | 目的 |
---|---|---|
foreach | 終端 | 消費流中的每個元素并對其應用 Lambda。這一操作返回 void |
count | 終端 | 返回流中元素的個數。這一操作返回 long |
collect | 終端 | 把流歸約成一個集合,比如 List 、 Map 甚至是 Integer |
Stream是一個非常好用的一個新特性,它能幫助我們簡化很多冗長的代碼,提高我們代碼的可讀性。
本章總結流是“從支持數據處理操作的源生成的一系列元素”。
流利用內部迭代:迭代通過filter、map、sorted等操作被抽象掉了。
流操作有兩類:中間操作和終端操作。
filter和map等中間操作會返回一個流,并可以鏈接在一起。可以用它們來設置一條流水線,但并不會生成任何結果。
forEach和count等終端操作會返回一個非流的值,并處理流水線以返回結果。
6.流中的元素是按需計算(懶加載)的。
代碼示例Github: chap4
Gitee: chap4
公眾號如果,你對Java8中的新特性很感興趣,你可以關注我的公眾號或者當前的技術社區的賬號,利用空閑的時間看看我的筆記,非常感謝!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76849.html
摘要:實戰讀書筆記第一章從方法傳遞到接著上次的,繼續來了解一下,如果繼續簡化代碼。去掉并且生成的數字是萬,所消耗的時間循序流并行流至于為什么有時候并行流效率比循序流還低,這個以后的文章會解釋。 《Java8實戰》-讀書筆記第一章(02) 從方法傳遞到Lambda 接著上次的Predicate,繼續來了解一下,如果繼續簡化代碼。 把方法作為值來傳遞雖然很有用,但是要是有很多類似與isHeavy...
摘要:第三個問題查找所有來自于劍橋的交易員,并按姓名排序。第六個問題打印生活在劍橋的交易員的所有交易額。第八個問題找到交易額最小的交易。 付諸實戰 在本節中,我們會將迄今學到的關于流的知識付諸實踐。我們來看一個不同的領域:執行交易的交易員。你的經理讓你為八個查詢找到答案。 找出2011年發生的所有交易,并按交易額排序(從低到高)。 交易員都在哪些不同的城市工作過? 查找所有來自于劍橋的交易...
摘要:第四章引入流一什么是流流是的新成員,它允許你以聲明性方式處理數據集合通過查詢語句來表達,而不是臨時編寫一個實現。 第四章 引入流 一、什么是流 流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷數據集的高級迭代器。此外,流還可以透明地并行處理,你無需寫任何多線程代碼。 下面兩段代碼都是用來返回低...
摘要:跳過元素流還支持方法,返回一個扔掉了前個元素的流。歸約到目前為止,我們見到過的終端操作都是返回一個之類的或對象等。這樣的查詢可以被歸類為歸約操作將流歸約成一個值。通過反復使用加法,你把一個數字列表歸約成了一個數字。 使用流 在上一篇的讀書筆記中,我們已經看到了流讓你從外部迭代轉向內部迭代。這樣,你就用不著寫下面這樣的代碼來顯式地管理數據集合的迭代(外部迭代)了: /** * 菜單 ...
摘要:收集器用作高級歸約剛剛的結論又引出了優秀的函數式設計的另一個好處更易復合和重用。更具體地說,對流調用方法將對流中的元素觸發一個歸約操作由來參數化。另一個常見的返回單個值的歸約操作是對流中對象的一個數值字段求和。 用流收集數據 我們在前一章中學到,流可以用類似于數據庫的操作幫助你處理集合。你可以把Java 8的流看作花哨又懶惰的數據集迭代器。它們支持兩種類型的操作:中間操作(如 filt...
閱讀 3351·2021-10-13 09:40
閱讀 2586·2021-10-08 10:17
閱讀 3989·2021-09-28 09:45
閱讀 921·2021-09-28 09:35
閱讀 1805·2019-08-30 10:51
閱讀 2897·2019-08-26 12:11
閱讀 1644·2019-08-26 10:41
閱讀 3090·2019-08-23 17:10