前言
集合是Java中使用最多的API。要是沒有集合,還能做什麼呢?幾乎每個Java應用程序都會制 造和處理集合。集合對於很多編程任務來說都是非常基本的:它們可以讓你把數據分組並加以處 理。
-
很多業務邏輯都涉及類似於數據庫的操作,比如對幾道菜按照類別進行分組 (比如全素 菜餚),或查找出最貴的菜。你自己用迭代器重新實現過這些操作多少遍?大部分數據庫 都允許你聲明式地指定這些操作。比如,以下SQL查詢語句就可以選出熱量較低的菜餚名 稱: SELECT name FROM dishes WHERE calorie < 400 。你看,你不需要實現如何 根據菜餚的屬性進行篩選(比如利用迭代器和累加器),你只需要表達你想要什麼。這個 基本的思路意味着,你用不着擔心怎麼去顯式地實現這些查詢語句——都替你辦好了! 怎麼到了集合這裏就不能這樣了呢?
-
要是要處理大量元素又該怎麼辦呢?爲了提高性能,你需要並行處理,並利用多核架構。 但寫並行代碼比用迭代器還要複雜,而且調試起來也夠受的!
那Java語言的設計者能做些什麼,來幫助你節約寶貴的時間,讓你這個程序員活得輕鬆一點兒呢?你可能已經猜到了,答案就是流。
流 是什麼
流(Stream)是javaAPI的新成員,它允許你以聲明性方式處理數據集(通過查詢語句來表達而不是臨時編寫一個實現). 此外,流還可以並行的進行處理,你無須寫任何多線程代碼了.
首先,我們以一個例子看下流的使用:下面兩段代碼都是用來返回低熱量的菜餚名稱的, 並按照卡路里排序,一個是用Java 7寫的,另一個是用Java 8的流寫的。比較一下。不用太擔心 Java 8代碼怎麼寫,我們在接下來的幾節裏會詳細解釋。
實體類 Dish.java
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
//get/set 略
}
java7中實現
/**
* java7
*/
@Test
public void test1() {
//選出低熱量菜餚
ArrayList<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish dish : menu) {
if (dish.getCalories() < 400) {
lowCaloricDishes.add(dish);
}
}
//按照卡路里進行排序
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
@Override
public int compare(Dish dish1, Dish dish2) {
return Integer.compare(dish1.getCalories(), dish2.getCalories());
}
});
//輸出菜餚名稱
ArrayList<String> lowCaloricDishesName = new ArrayList<>();
for (Dish dish : lowCaloricDishes) {
lowCaloricDishesName.add(dish.getName());
}
System.out.println(lowCaloricDishesName);
}
在這段代碼中,你用了一個“垃圾變量” lowCaloricDishes 。它唯一的作用就是作爲一次 性的中間容器。在Java 8中,實現的細節被放在它本該歸屬的庫裏了。
java8
/**
* java8
*/
@Test
public void test2() {
List<String> lowCaloricDishesName =
menu.stream()
//選出低熱量菜餚
.filter(d -> d.getCalories() < 400)
//按照卡路里進行排序
.sorted(Comparator.comparing(Dish::getCalories))
//輸出菜餚名稱
.map(Dish::getName)
.collect(toList());
System.out.println(lowCaloricDishesName);
}
爲了利用多核架構並行執行這段代碼,你只需要把 stream() 換成 parallelStream() :
/**
* java8多核架構並行執行這段代碼
*/
@Test
public void test3() {
List<String> lowCaloricDishesName =
menu.parallelStream()
//選出低熱量菜餚
.filter(d -> d.getCalories() < 400)
//按照卡路里進行排序
.sorted(Comparator.comparing(Dish::getCalories))
//輸出菜餚名稱
.map(Dish::getName)
.collect(toList());
System.out.println(lowCaloricDishesName);
}
現在,你可以看出,從軟件工程師的角度來看,新 的方法有幾個顯而易見的好處:
-
代碼是以聲明性方式寫的:說明想要完成什麼,而不是說明如何實現一個操作(利用for if 等控制語句).這種方法加上行爲參數化,可以讓你很輕鬆的應對變化的需求,你很容易再創建一個代碼版本,利用 Lambda表達式來篩選高卡路里的菜餚,而用不着去複製粘貼代碼
-
你可以把幾個基礎操作連接起來:來表達複雜的數據流水線工作,同時保證代碼清晰可讀.filter 的結果被傳給了 sorted 方法,再傳給 map 方法,最後傳給 collect 方法。
需要注意的是: filter(),sorted(),map(), 返回的都是流(Stream),都是Stream的方法,collect()方法除外.
流簡介
java8中的集合支持一個新的stream()方法,它會返回一個流,接口定義在 java.util.stream.Stream中.
那麼,流到底是什麼呢?簡短的定義就是“從支持數據處理操作的源生成的元素序列”。讓我們一步步剖析這個定義:
-
元素序列: 就像集合一樣,提供了一個接口,可以訪問特定元素類型的一組有序值.因爲集合是數據結構,所以它的主要目的是以特定的時間/空間複雜度來存儲訪問元素.但流的目的在於表達計算.
-
源: 流會使用一個提供數據的源,這些源可以是 數組,集合,或輸入輸出資源.注意:從有序結合生成的流會保留原有的順序,由列表生成的流,其元素順序也與列表一致.
-
數據處理操作: 流的數據處理功能類似於數據庫的操作.以及函數式編程語言的常用操作.如 filter 、 map 、 reduce 、 find 、 match 、 sort 等。流操作可以順序執行,也可並行執行。 此外,流操作有兩個重要的特點。
-
流水線: 很多流操作本身會返回一個流.這樣多個操作就可以連接起來形成一個更大的流水線.流水線操作可以看成對數據源進行數據庫式查詢.
-
內部迭代: 與使用迭代器對集合進行顯示迭代不同,流的迭代都是在背後進行的.
讓我們來看一段能夠體現所有這些概念的代碼:
@Test
public void test4() {
List<String> lowCaloricDishesName =
//1.從 menu 獲得流(菜餚列表),建立操作流水線
menu.parallelStream()
//2.選出高熱量菜餚
.filter(d -> d.getCalories() > 300)
//3.輸出菜餚名稱
.map(Dish::getName)
//4.只選擇前三個
.limit(3)
//5.將結果保存在另一個List中
.collect(toList());
System.out.println(lowCaloricDishesName);
}
在本例中,我們顯示對menu進行stream操作,得到一個流,數據源是菜餚列表menu,接下來對流進行一系列數據處理操作:filter 、 map 、 limit 和 collect 。除了 collect 之外,所有這些操作都會返回另一個流,這樣它們就可以接成一條流水線,於是就可以看作對源的一個查詢. 最後collect開始處理流水線,並返回一個結果(collect和別的操作不一樣,它返回的不是一個流而是一個list). 在調用collect之前,沒有任何結果產生,事實上,根本就沒有從menu裏選擇元素.你可以這麼理解:鏈中的方法調用都在排隊等待,直到調用 collect
圖4-2顯示了流操作的順序: filter 、 map 、 limit 、 collect , 每個操作簡介如下。
在進一步介紹能對流做什麼操作之前,先讓我們回過頭來看看Collection API和新的Stream API的思想有何不同.
流與集合
粗略的講,流與集合的差異就在於什麼時候進行計算,集合是內存中的數據結構,它包含數據結構中目前所有的值(結合中每個元素必須先計算出來才能添加到集合中.)
(你可以往集合里加東西或者刪東西,但是不管什麼時候,集合中的每個元素都是放在內存裏的,元素都得先算出來才能成爲集合的一部分。)
相比之下,流是再概念上固定的數據結構.這個思想就是用戶僅僅從流中提取需要的值,而這些值,在用戶看不見的地方,只會按需生成. 這是一種 生產者–消費者 的關係,從另一個角度來說,流就想一個延遲創建的集合:只有在消費者要求的時候纔會計算值(用管理學的話說這就是需求驅動,甚至是實時製造)。
與此相反,集合則是急切創建的(供應商驅動:先把倉庫裝滿,再開始賣,就像那些曇花一 現的聖誕新玩意兒一樣)。以質數爲例,要是想創建一個包含所有質數的集合,那這個程序算起 來就沒完沒了了,因爲總有新的質數要算,然後把它加到集合裏面。當然這個集合是永遠也創建 不完的,消費者這輩子都見不着了.
圖4-3用DVD對比在線流媒體的例子展示了流和集合之間的差異
只能遍歷一次
請注意,和迭代器一樣,流只能遍歷一次,遍歷完之後,我們就說這個流已經被消費掉了, 你可以從原始數據源那裏再獲得一個新的流來重新遍歷一遍,就像迭代器一樣(這裏假設它是集 合之類的可重複的源,如果是I/O通道就沒戲了)。例如,以下代碼會拋出一個異常,說流已被消 費掉了:java.lang.IllegalStateException: stream has already been operated upon or closed
@Test
public void test5(){
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> stream = title.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println);
}
外部迭代與內部迭代
內部迭代時,項目可以透明地並行處理,或者用更優化的順 序進行處理。要是用Java過去的那種外部迭代方法,這些優化都是很困難的。這似乎有點兒雞蛋 裏挑骨頭,但這差不多就是Java 8引入流的理由了——Streams庫的內部迭代可以自動選擇一種適 合你硬件的數據表示和並行實現。
流操作
java.util.stream.Stream 中的 Stream 接口定義了許多操作。它們可以分爲兩大類. 可以連接起來的流操作稱爲中間操作,關閉流的操作稱爲終端操作
中間操作
注入filter和sorted等中間操作會返回另外一個流,這讓多箇中間操作可以連接起來形成一個查詢.重要的是: 除非流水線上觸發一個終端操作,否則中間操作不會執行任何一個處理–他們很懶,這是因爲中間操作一般都可以合併起來,在終端操作時一次性全部處理
爲了搞清楚流水線中到底發生了什麼,我們把代碼改一改,讓每個Lambda都打印出當前處 理的菜餚:
@Test
public void test6() {
List<String> names =
//1.從 menu 獲得流(菜餚列表),建立操作流水線
menu.stream()
//2.選出高熱量菜餚
.filter(d -> {
System.out.println("filtering" + d.getName());
return d.getCalories() > 300;
})
//3.輸出菜餚名稱
.map(d -> {
System.out.println("mapping" + d.getName());
return d.getName();
})
//4.只選擇前三個
.limit(3)
//5.將結果保存在另一個List中
.collect(toList());
System.out.println(names);
}
此代碼執行時將打印:
filteringpork
mappingpork
filteringbeef
mappingbeef
filteringchicken
mappingchicken
[pork, beef, chicken]
你會驚訝的發現,流操作並沒有打印出我們預期的信息.這是因爲有好幾種優化利用了流的延遲性質.
第一:儘管很多菜的熱量都高於300卡路里,但只選出了前三個!這是因爲 limit 操作和一種稱爲短路的技巧,我們會在下一章中解釋
第二,儘管 filter 和 map 是兩個獨立的操作,但它們合併到同一次遍歷中了(我們把這種技術叫作循環合併)。
終端操作
終端操作會從流的流水線生成結果.其結果是任何不是流的值,比如List,Integer,甚至是void.
例如在下面的流水線中,foerach是一個返回void的終端操縱,它會對源中的每道菜應用一個Lambda,把 System.out.println 傳遞給 forEach ,並要求它打印出由 menu 生成的流中的每一個 Dish :
menu.stream().forEach(System.out::println);
小結:
-
流是“從支持數據處理操作的源生成的一系列元素”
-
流利用內部迭代:迭代通過 filter 、 map 、 sorted 等操作被抽象掉了。
-
流操作有兩類:中間操作和終端操作。
-
filter 和 map 等中間操作會返回一個流,並可以鏈接在一起。可以用它們來設置一條流 水線,但並不會生成任何結果。
-
forEach 和 count 等終端操作會返回一個非流的值,並處理流水線以返回結果
-
流中的元素是按需計算的