流是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方法時到底發生了什麼。用了多少個線程?對性能有多大的提高?不用着急,在後面的讀書筆記中會討論這些問題。如今,你能夠看出,從軟件工程師的角度來看,新的方法有幾個顯而易見的好處。編程
filter、sorted、map和collect等操做是與具體線程模型無關的高層次構件,因此它們的內部實現能夠是單線程的,也可能透明地充分利用你的多核架構!在實踐中,這意味着咱們用不着爲了讓某些數據處理任務並行而去操心線程和鎖了,Stream API都替你作好了!數組
如今就來仔細探討一下怎麼使用Stream API。咱們會用流與集合作類比,作點兒鋪墊。下一 章會詳細討論能夠用來表達複雜數據處理查詢的流操做。咱們會談到不少模式,如篩選、切片、 查找、匹配、映射和歸約,還會提供不少測驗和練習來加深你的理解。接下來,咱們會討論如何建立和操縱數字流,好比生成一個偶數流,或是勾股數流。最後,咱們會討論如何從不一樣的源(好比文件)建立流。還會討論如何生成一個具備無窮多元素的流,這用集合確定是搞不定。bash
要討論流,咱們首先來談談集合,這是最容易上手的方式了。Java8中的集合支持一個新的stream方法,它會返回一個流(接口定義在 java.util.stream.Stream 裏)。你在後面會看到,還有不少其餘的方法能夠獲得流,好比利用數值範圍或從I/O資源生成流元素。數據結構
那麼,流究竟是什麼呢?簡短的定義就是「從支持數據處理操做的源生成的元素序列」。讓咱們一步步剖析這個定義。多線程
此外,流操做有兩個重要的特色。架構
讓咱們來看一段可以體現全部這些概念的代碼:
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);
複製代碼
看起來很簡單,就算不明白也不要緊,咱們來了解了解,剛剛使用到的一些方法:
d -> d.getCalories() > 300
,選擇出熱量高於300卡路里的菜餚。Dish::getName
,提取了每道菜的菜名。在剛剛解釋的這段代碼,與遍歷處理菜單集合的代碼有很大的不一樣。首先,咱們使用了聲明性的方式來處理菜單數據。咱們並無去實現篩選(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!下面的圖說明了流(內部迭代)與集合(外部迭代)之間的差別。
咱們已經瞭解過了集合與流在概念上的差別,特別是利用內部迭代:替你把迭代作了。可是,只有你已經預先定義好了可以隱藏迭代的操做集合。例如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的值。所以,它是終端操做。
總而言之,流的使用通常包括三件事:
流的流水線背後的理念相似於構建器模式。 在構建器模式中有一個調用鏈用來設置一套配置(對流來講這就是一箇中間操做鏈),接着是調用built方法(對流來講就是終端操做)。其實,咱們目前所看的Stream的例子用到的方法並非它的所有,還有一些其餘的一些操做。
在本章中,咱們所接觸到的一些中間操做與終端操做:
中間:
操做 | 類型 | 返回類型 | 操做參數 | 函數描述 |
---|---|---|---|---|
filter | 中間 | Stream | Predicate | T -> boolean |
map | 中間 | Stream | Function<T, R> | T -> R |
limit | 中間 | Stream | ||
sorted | 中間 | Stream | Comparator | (T, T) -> int |
distinct | 中間 | Stream |
終端:
操做 | 類型 | 目的 |
---|---|---|
foreach | 終端 | 消費流中的每一個元素並對其應用 Lambda。這一操做返回 void |
count | 終端 | 返回流中元素的個數。這一操做返回 long |
collect | 終端 | 把流歸約成一個集合,好比 List 、 Map 甚至是 Integer |
Stream是一個很是好用的一個新特性,它能幫助咱們簡化不少冗長的代碼,提升咱們代碼的可讀性。
Github: chap4
Gitee: chap4
若是,你對Java8中的新特性很感興趣,你能夠關注個人公衆號或者當前的技術社區的帳號,利用空閒的時間看看個人筆記,很是感謝!