使用流操作來表達復雜的數(shù)據(jù)處理查詢。
集合是Java中使用多的API。要是沒有集合,還能做什么呢?幾乎每個Java應用程序都會制 造和處理集合。集合對于很多編程任務來說都是非?;镜模核鼈兛梢宰屇惆褦?shù)據(jù)分組并加以處理。例如,你希望根據(jù)創(chuàng)建一個銀行的交易集合來分析用戶的消費行為。所以,你希望通過處理整個集合來了解用戶的消費的消費情況,這在銀行業(yè)務方面很常見,也很重要,但是java在集合操作方面卻遠遠算不上完美。
- 首先,典型集合處理方式類似SQL的操作,例如“查找”(例如,查找最大的一筆消費)或“分組”(例如,將與雜貨店購物相關的所有交易分組)。 大多數(shù)數(shù)據(jù)庫允許你以聲明方式做此類操作。 例如,以下SQL查詢語句就可以選出最大一筆消費的事務ID:“SELECT id,MAX(value)from transactions”。
- 其次,要是要處理大量元素又該怎么辦呢?為了提高性能,你需要并行處理,并利用多核架構。 但寫并行代碼比用迭代器還要復雜,而且調試起來也夠受的!
Java SE 8來了! Java API設計人員增加stream的新成員,它允許你以聲明性方式處理數(shù)據(jù)集合(通過查詢語句來表達,而不是臨時編寫一個實現(xiàn))。此外,流可以利用多核架構,你無需寫任何多線程代碼了! 聽起來不錯,不是嗎? 這就是本節(jié)將要探討的內容。
在我們詳細探討stream可以做什么之前,我們先寫兩個簡單的例子來對比一下?,F(xiàn)在有如下需求,假設我們需要查找grocery類型的所有transactions ,并返回按transaction value的降序排序的transaction ID列表。 在Java SE 7中,我們一般會如下操作:
例1
//extract transactions of type matching parameter type
List<Transaction> groceryTransactions = new ArrayList<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
//sort by value descending
Collections.sort(groceryTransactions, new Comparator<Transaction>(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
//get transaction IDs
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionIds.add(t.getId());
}
采用Java SE 8 streams的編程風格,代碼如下:
例2
List<Integer> transactionsIds = transactions.stream() //user transactions ArrayList as source
.filter(t -> t.getType().equals(Transaction.GROCERY)) //filter on type
.sorted(comparing(Transaction::getValue).reversed()) //sort using value, but reversed
.map(Transaction::getId) //extract IDs
.collect(toList()); //save in List
圖1顯示了Java SE 8代碼。我們使用List的stream()方法從transactions集合鐘獲取流,接下來,將幾個操作(過濾,排序,映射,收集)鏈接在一起以形成一個管道,該過程可以視為數(shù)據(jù)的查詢。

那么如何并行化查詢呢? 在Java SE 8中,它很容易:只需要將stream() 替換成parallelStream() 即可,Streams API 根據(jù)你計算機的內核自動分解成多個任務并行。如下所示:
例3
List<Integer> transactionsIds = transactions.parallelStream() //parallelizing the code
.filter(t -> t.getType().equals(Transaction.GROCERY))
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
如果你現(xiàn)在看著有點稀里糊涂,沒關系,你只需要熟悉lambda表達式(t -> t.getType().equals(Transaction.GROCERY)Y)和方法引用(Transaction::getId)就好了,原理將會在后面詳細講解。
現(xiàn)在你只需要將流看作類似于sql查詢的一種抽象的高效的集合操作。 此外,可以使用lambda表達式簡潔地對這些操作進行參數(shù)化。
開始流的學習
讓我們先從理論開始學習吧, 流的是什么? 一個簡短的定義是“Stream是一個來自支持聚合操作源的元素的序列“。流簡單的來說有如下特點:
- 元素序列:一個Stream向外提供了一個這樣的接口:特定元素類型的值的序列,但是Stream并不實際持有,也就是說不存儲這些元素,它們是在有需求時才會被計算。
- 源:以提供Stream進行計算消費的源,這些源有Collection集合 Array數(shù)組或I/O資源等。
-
聚合操作:Stream支持類似SQL操作和函數(shù)式編程的大部分操作,比如:filter, map, reduce, find, match, sorted。
此外,Stream操作不同于Collection操作有兩個根本的地方: - 管道Pipelining:許多流Stream操作返回流Stream自身,這就允許對其操作可以像鏈條一樣排列,變成一個管道,這其中也會激活比如懶加載和short-circuiting操作。
-
內部迭代:相比于集合Collection是顯式迭代(需要我們編碼完成迭代),Stream操作是在其內部完成迭代操作。
圖2 顯示上面代碼2中 Stream代碼的內部工作流程:
圖2
圖2 顯示:
我們首先通過調用stream()從交易transactions List中獲得Stream對象,數(shù)據(jù)源就是transactions List,為流提供了一系列可操作元素。接下來,我們對流進行了一系列的聚合操作:filter(通過predicate過濾元素),sorted(通過comparator進行排序)和map(將transactions 轉化成 ids)。 除了collect之外的所有這些操作都返回一個Stream,因此它們可以被鏈接成一個管道,可以看成是基于源數(shù)據(jù)集合的一個查詢操作。如同SQL基于數(shù)據(jù)表的有條件查詢語句一樣。
在調用collect之前,所有聚合操作實際上沒有工作。collect操作開始正式處理pipeline并返回一個結果(返回的結果并不一定全部都是stream,比如這里就返回一個List),我們先不用理解collert的原理。我們先將collect視為一個操作,該操通過不同的參數(shù)(toSet(),toList(),toCollection),將流中的元素累計匯總到一個集合當中,這里參數(shù)是toList(),所以將Stream 轉化成了一個List。
在我們深入學習stream的各種使用方式之前,我們需要先搞清楚Stream和Collections之間的區(qū)別。
Streams VS Collections
Java現(xiàn)有的集合概念和新的流概念都提供了接口,來配合代表元素型有序值的數(shù)據(jù)接口。所 謂有序,就是說我們一般是按順序取用值,而不是隨機取用的。那這兩者有什么區(qū)別呢?簡而言之,集合是關于數(shù)據(jù)的,而流是關于計算的。
我們先來打個直觀的比方吧。比如說存在DVD里的電影,這就是一個集合(也許是字節(jié),也 許是幀,這個無所謂),因為它包含了整個數(shù)據(jù)結構。現(xiàn)在再來想想在互聯(lián)網(wǎng)上通過視頻流看同 樣的電影?,F(xiàn)在這是一個流(字節(jié)流或幀流)。流媒體視頻播放器只要提前下載用戶觀看位置的 那幾幀就可以了,這樣不用等到流中大部分值計算出來,你就可以顯示流的開始部分了(想想觀 看直播足球賽)。特別要注意,視頻播放器可能沒有將整個流作為集合,保存所需要的內存緩沖 區(qū)——而且要是非得等到后一幀出現(xiàn)才能開始看,那等待的時間就太長了。出于實現(xiàn)的考慮, 你也可以讓視頻播放器把流的一部分緩存在集合里,但和概念上的差異不是一回事。 粗略地說,集合與流之間的差異就在于什么時候進行計算。集合是一個內存中的數(shù)據(jù)結構, 它包含數(shù)據(jù)結構中目前所有的值——集合中的每個元素都得先算出來才能添加到集合中。(你可以往集合里加東西或者刪東西,但是不管什么時候,集合中的每個元素都是放在內存里的,元素都得先算出來才能成為集合的一部分。) 相比之下,流則是在概念上固定的數(shù)據(jù)結構(你不能添加或刪除元素),其元素則是按需計算的。
圖3用DVD對比在線流媒體的例子展示了流和集合之間的差異。

使用Collection接口需要用戶去做迭代(比如用for-each),這稱為外部迭代。 相反, Streams庫使用內部迭代——它幫你把迭代做了,還把得到的流值存在了某個地方,你只要給出 一個函數(shù)說要干什么就可以了。下面的代碼列表說明了這種區(qū)別。
例4
List<String> transactionIds = new ArrayList<>();
for(Transaction t: transactions){
transactionIds.add(t.getId());
}
例5
List<Integer> transactionIds =
transactions.stream()
.map(Transaction::getId)
.collect(toList());
在例4中,我們按順序顯式迭代transactions列表以提取每個事務ID并將其添加到集合當中。 相反,當使用流時,沒有明確的迭代。 例5中的代碼構建了一個查詢,其中參數(shù)化map操作以提取事務ID,并且collect操作將生成的Stream轉換為List。

至此,你應該明白流是什么了,接下來我們介紹流的具體使用方式。
流操作
java.util.stream.Stream中的Stream接口定義了許多操作。它們可以分為兩大類。在例2中,我們有如下幾個操作:
- filter、map和limit可以連成一條流水線;
- collect觸發(fā)流水線執(zhí)行并關閉它。
可以連接起來的流操作稱為中間操作(intermediate operations),關閉流的操作稱為終端操作(terminal operations)。
中間操作,諸如filter或sorted等中間操作會返回另一個流。這讓多個操作可以連接起來形成一個查 詢。重要的是,除非流水線上觸發(fā)一個終端操作,否則中間操作不會執(zhí)行任何處理——它們很懶。 這是因為中間操作一般都可以合并起來,在終端操作時一次性全部處理。
終端操作會從流的流水線生成結果。其結果是任何不是流的值,比如List、Integer,甚 至void。
例6
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares = numbers.stream()
.filter(n -> {
System.out.println("filtering " + n);
return n % 2 == 0; //returns true if n is even
})
.map(n -> {
System.out.println("mapping " + n);
return n * n; //square n
})
.limit(2) //only return the first two squared evens
.collect(Collectors.toList()); //save in List
輸出如下:
filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4
你可能對結果很驚訝?為什么只記算2個偶數(shù)的平方數(shù)呢?這是因為limit(2)操作和一種稱為短路的技巧(我們后面會詳細介紹),我們只需要處理部分流而不是全部流來返回結果。這類似于使用and運算符鏈接的布爾表達式,只要一個表達式返回false,我們就可以推斷出整個表達式為false,而不用全部記算完畢。例6中,limit限制返回得size為2. 其次,盡管filter和map是兩個獨立的操作,但它們合并到同一次遍歷中了(我們把這種技術叫作循環(huán)合并)。
| 操 作 | 類 型 | 返回類型 | 操作參數(shù) | 函數(shù)描述符 |
|---|---|---|---|---|
| filter | 中間 | Stream<T> | Predicate<T> | T -> boolean |
| distinct | 中間 | Stream<T> | ||
| skip | 中間 | Stream<T> | long | |
| limit | 中間 | Stream<T> | ||
| map | 中間 | Stream<R> | Function<T, R> | T -> R |
| flatMap | 中間 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
| sorted | 中間 | Stream<T> | Comparator<T> | (T, T) -> int |
| anyMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| noneMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| allMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| findAny | 終端 | Optional<T> | ||
| findFirst | 終端 | Optional<T> | ||
| forEach | 終端 | void | Consumer<T> | T -> void |
| collect | 終端 | R | Collector<T, A, R> | |
| reduce | 終端 | Optional<T> | BinaryOperator<T> | (T, T) -> T |
| count | 終端 | long |
總結一下到目前為止我們學到的知識,一般來說,流的使用一般包括三件事:
- 一個數(shù)據(jù)源(如集合)來執(zhí)行一個查詢;
- 一個中間操作鏈,形成一條流的流水線;
- 一個終端操作,執(zhí)行流水線,并能生成結果。
接下來讓我們學習一下stream的一些常用操作,更多內容請參閱java.util .stream.Stream. -
過濾
對流中的元素進行過濾操作,有如下幾個操作: - filter(Predicate): predicate (java.util.function.Predicate)
- distinct: 它會返回一個元素各異(根據(jù)流所生成元素的 hashCode和equals方法實現(xiàn))的流.
- limit(n): 該方法會返回一個不超過給定長度的流。所需的長度作為參數(shù)傳遞 給limit。如果流是有序的,則多會返回前n個元素。
-
skip(n): * 返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一 個空流。
查找和匹配
另一個常見的數(shù)據(jù)處理套路是看看數(shù)據(jù)集中的某些元素是否匹配一個給定的屬性。Stream API通過allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了這樣的工具。它們都將predicate作為參數(shù)并返回一個boolean作為結果(因此,它們是終端操作)。 比如, 你可以使用allMatch*去匹配transactions的stream中的value值大于100的元素 。如下所示:
例7
boolean expensive =
transactions.stream()
.allMatch(t -> t.getValue() > 100);
此外,Stream接口提供了findFirst和findAny方法,用于從流中檢索任意元素。它們可以和流的其他方法結合使用,比如filter。findFirst和findAny都會返回一個Optional對象,如下所示:
Optional<Transaction> any = transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny();
何時使用findFirst和findAny 你可能會想,為什么會同時有findFirst和findAny呢?答案是并行。找到第一個元素 在并行上限制更多。如果你不關心返回的元素是哪個,請使用findAny,因為它在使用并行流 時限制較少。
Optional<T>類(java.util.Optional)是一個容器類,代表一個值存在或不存在。在 上面的代碼中,findAny可能什么元素都沒找到。。Java 8的庫設計人員引入了Optional<T>,這 樣就不用返回眾所周知容易出問題的null了。我們在這里不會詳細討論Optional,以后會詳細解釋你的代碼如何利用Optional,避免和null檢查相關的bug。不過現(xiàn)在,了解一下Optional里面幾種可以迫使你顯式地檢查值是否存在或處理值不存在的情形的方法也不錯。
isPresent():將在Optional包含值的時候返回true, 否則返回false。
ifPresent(Consumer<T> block):會在值存在的時候執(zhí)行給定的代碼塊。
T get():會在值存在時返回值,否則拋出一個NoSuchElement異常。
- T orElse(T other)*: 會在值存在時返回值,否則返回一個默認值。
例如,如果存在Transaction,我們可以選擇使用ifPresent方法對可選對象進行操作,如例9所示(打印事務):
- T orElse(T other)*: 會在值存在時返回值,否則返回一個默認值。
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny()
.ifPresent(System.out::println);
映射(Mapping)
流支持map方法,它會接受一個function(java.util.function.Function)作為參數(shù)。這個函數(shù)會被應用到每個元素上,并將其映射成一個新的元素(使用映射一詞,是因為它和轉換類似,但其中的細微差別在于它是“創(chuàng)建一 個新版本”而不是去“修改”)。
例如,我們想要將流中的元素提取屬性值,如例10中,我們返回單詞列表中的長度列表:
例10
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
List<Integer> wordLengths =
words.stream()
.map(String::length)
.collect(toList());
歸約(Reducing)
到目前為止,你見到過的終端操作都是返回一個boolean(allMatch之類的)、void (forEach)或Optional對象(findAny等)。你也見過了使用collect來將流中的所有元素組 合成一個List。
你還可以把一個流中的元素組合起來,使用reduce操作來表達更復雜的查 詢,比如“計算Transaction中的總value值”或“查詢Transaction中最大的ID”。此類查詢需要將流中所有元素反復結合起來,得到一個值,比如一個Integer。這樣的查詢可以被歸類為歸約操作 (將流歸約成一個值)。用函數(shù)式編程語言的術語來說,這稱為折疊(fold),因為你可以將這個操 作看成把一張長長的紙(你的流)反復折疊成一個小方塊,而這就是折疊操作的結果。
在我們研究如何使用reduce方法之前,先來看看如何使用for-each循環(huán)來對數(shù)字列表中的 元素求和:
int sum = 0;
for (int x : numbers) {
sum += x;
}
numbers中的每個元素都用加法運算符反復迭代來得到結果。通過反復使用加法,你把一個 數(shù)字列表歸約成了一個數(shù)字。這段代碼中有兩個參數(shù):
- 總和變量的初始值,在這里是0;
- 將列表中所有元素結合在一起的操作,在這里是+。
要是能把所有的數(shù)字相加,而不必去復制粘貼這段代碼,豈不是很好?這正是reduce操 作的用武之地,它對這種重復應用的模式做了抽象。你可以像下面這樣對流中所有的元素求和:
例11
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce接受兩個參數(shù):
- 一個初始值,這里是0;
- 一個BinaryOperator<T>來將兩個元素結合起來產(chǎn)生一個新值,這里我們用的是 lambda (a, b) -> a + b.
你也很容易把所有的元素相乘,只需要將另一個Lambda:(a, b) -> a * b傳遞給reduce 操作就可以了:
例12
int product = numbers.stream().reduce(1, (a, b) -> a * b);
下圖展示了reduce操作是如何作用于一個流的:Lambda反復結合每個元素,直到流被歸約 成一個值。

你可以使用方法引用讓這段代碼更簡潔。在Java 8中,Integer類現(xiàn)在有了一個靜態(tài)的sum 方法來對兩個數(shù)求和,這恰好是我們想要的,用不著反復用Lambda寫同一段代碼了:
int sum = numbers.stream().reduce(0, Integer::sum);
數(shù)值流(Numeric Streams)
我們在前面看到了可以使用reduce方法計算流中元素的總和。例如,你可以像下面這樣計算Transaction集合的value總和:
double statement =
transactions.stream()
.map(Transaction::getValue)
.reduce(0.0, Double::sum);
這段代碼的問題是,它有一個暗含的裝箱成本。每個Integer都必須拆箱成一個原始類型, 再進行求和。要是可以直接像下面這樣調用sum方法,豈不是更好?
例13
double statement =
transactions.stream()
.map(Transaction::getValue)
.sum(); // error since Stream has no sum method
但這是不可能的。問題在于map方法會生成一個Stream<T>。雖然流中的元素是Integer類 型,但Streams接口沒有定義sum方法。為什么沒有呢?比方說,你只有一個像Transaction那樣的 Stream<Transaction>,把所以Transaction加起來是沒有任何意義的。但不要擔心,Stream API還提供了原始類 型流特化,專門支持處理數(shù)值流的方法。
Java 8引入了三個原始類型特化流接口來解決這個問題:IntStream、DoubleStream和 LongStream,分別將流中的元素特化為int、long和double,從而避免了暗含的裝箱成本。每個接口都帶來了進行常用數(shù)值歸約的新方法,比如對數(shù)值流求和的sum,找到大元素的max。 此外還有在必要時再把它們轉換回對象流的方法。要記住的是,這些特化的原因并不在于流的復雜性,而是裝箱造成的復雜性——即類似int和Integer之間的效率差異。
將流轉換為特化版本的常用方法是mapToInt、mapToDouble和mapToLong。這些方法和前 面說的map方法的工作方式一樣,只是它們返回的是一個特化流,而不是Stream<T>。例如我們可以將例13改造一下,如下所示:
例14
double statementSum =
transactions.stream()
.mapToDouble(Transaction::getValue)
.sum(); // works!
同樣,一旦有了數(shù)值流,你可能會想把它轉換回非特化流。要把原始流轉換成一般流(每個double都會裝箱成一個 Double),可以使用boxed方法:
DoubleStream doubleStream = transactions.stream()
.mapToDouble(Transaction::getValue);
Stream<Double> stream = doubleStream.boxed();
數(shù)值范圍( numeric ranges)
和數(shù)字打交道時,有一個常用的東西就是數(shù)值范圍。比如,假設你想要生成1和100之間的所有數(shù)字。Java 8引入了兩個可以用于IntStream和LongStream的靜態(tài)方法,幫助生成這種范圍: range和rangeClosed。這兩個方法都是第一個參數(shù)接受起始值,第二個參數(shù)接受結束值。但 range是不包含結束值的,而rangeClosed則包含結束值。讓我們來看一個生成10到30之間偶數(shù)的例子:
例15
IntStream oddNumbers =
IntStream.rangeClosed(10, 30)
.filter(n -> n % 2 == 1);
構建流(Building Streams)
希望到現(xiàn)在,我們已經(jīng)讓你相信,流對于表達數(shù)據(jù)處理查詢是非常強大而有用的。到目前為 止,你已經(jīng)能夠使用stream方法從集合生成流了。此外,我們還介紹了如何根據(jù)數(shù)值范圍創(chuàng)建 數(shù)值流。但創(chuàng)建流的方法還有許多!本節(jié)將介紹如何從值序列、數(shù)組、文件來創(chuàng)建流,甚至由生成函數(shù)來創(chuàng)建無限流!
- 由值創(chuàng)建流
你可以使用靜態(tài)方法Stream.of,通過顯式值創(chuàng)建一個流。它可以接受任意數(shù)量的參數(shù)。例 如,以下代碼直接使用Stream.of創(chuàng)建了一個字符串流。然后,你可以將字符串轉換為大寫,再 一個個打印出來:
例16
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用empty得到一個空流,如下所示:
Stream<String> emptyStream = Stream.empty();
- 由數(shù)組創(chuàng)建流
你可以使用靜態(tài)方法Arrays.stream從數(shù)組創(chuàng)建一個流。它接受一個數(shù)組作為參數(shù)。例如, 你可以將一個原始類型int的數(shù)組轉換成一個IntStream,如下所示:
例17
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
- 由文件生成流
Java中用于處理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多靜態(tài)方法都會返回一個流。例如,一個很有用的方法是 Files.lines,它會返回一個由指定文件中的各行構成的字符串流。使用你迄今所學的內容, 你可以用這個方法看看一個文件中有多少各不相同的詞:
例18
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()), Charset.defaultCharset())){//流會自動 關閉
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()//刪除重復項
.count();//數(shù)一數(shù)有多少各不相同的單詞
}
catch(IOException e){
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
- 由函數(shù)生成流:創(chuàng)建無限流
Stream API提供了兩個靜態(tài)方法來從函數(shù)生成流:Stream.iterate和Stream.generate。 這兩個操作可以創(chuàng)建所謂的無限流:不像從固定集合創(chuàng)建的流那樣有固定大小的流。由iterate 和generate產(chǎn)生的流會用給定的函數(shù)按需創(chuàng)建值,因此可以無窮無盡地計算下去!一般來說, 應該使用limit(n)來對這種流加以限制,以避免打印無窮多個值。
我們先來看一個iterate的簡單例子,然后再解釋:
例19
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate方法接受一個初始值(在這里是0),還有一個依次應用在每個產(chǎn)生的新值上的 Lambda(UnaryOperator<t>類型)。這里,我們使用Lambda n -> n + 2,返回的是前一個元 素加上2。因此,iterate方法生成了一個所有正偶數(shù)的流:流的第一個元素是初始值0。然后加 上2來生成新的值2,再加上2來得到新的值4,以此類推。這種iterate操作基本上是順序的, 因為結果取決于前一次應用。請注意,此操作將生成一個無限流——這個流沒有結尾,因為值是 按需計算的,可以永遠計算下去。我們說這個流是無界的。正如我們前面所討論的,這是流和集 合之間的一個關鍵區(qū)別。我們使用limit方法來顯式限制流的大小。這里只選擇了前10個偶數(shù)。 然后可以調用forEach終端操作來消費流,并分別打印每個元素。
與iterate方法類似,generate方法也可讓你按需生成一個無限流。但generate不是依次 對每個新生成的值應用函數(shù)的。它接受一個Supplier<T>類型的Lambda提供新的值。我們先來 看一個簡單的用法:
例20
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
這段代碼將生成一個流,其中有五個0到1之間的隨機雙精度數(shù)。例如,運行一次得到了下面 的結果:
0.10697513719728047
0.9488665789337328
0.47239453991978764
0.49255993614793436
0.04943773738322066
Math.Random靜態(tài)方法被用作新值生成器。同樣,你可以用limit方法顯式限制流的大小, 否則流將會無限長。 你可能想知道,generate方法還有什么用途。我們使用的供應源(指向Math.random的方 法引用)是無狀態(tài)的:它不會在任何地方記錄任何值,以備以后計算使用。但供應源不一定是無 狀態(tài)的。你可以創(chuàng)建存儲狀態(tài)的供應源,它可以修改狀態(tài),并在為流生成下一個值時使用。
Lambda允許你創(chuàng)建函數(shù)式接口的實例,只要直接內聯(lián)提供方法的實 現(xiàn)就可以。你也可以像下面這樣,通過實現(xiàn)IntSupplier接口中定義的getAsInt方法顯式傳遞 一個對象(雖然這看起來是無緣無故地繞圈子,也請你耐心看):
例21
IntStream twos = IntStream.generate(new IntSupplier(){
public int getAsInt()
{
return 2;
}
});
twos.limit(10).forEach(System.out::println);
generate方法將使用給定的供應源,并反復調用getAsInt方法,而這個方法總是返回2。 但這里使用的匿名類和Lambda的區(qū)別在于,匿名類可以通過字段定義狀態(tài),而狀態(tài)又可以用 getAsInt方法來修改。這是一個副作用的例子。你迄今見過的所有Lambda都是沒有副作用的; 它們沒有改變任何狀態(tài).
結論
Java SE 8引入了Streams API,它允許你處理復雜的數(shù)據(jù)查詢。 在本文中,你已經(jīng)看到流支持許多操作,例如filter,map,reduce和iterate,它們可以組合起來編寫簡潔和富有表現(xiàn)力的數(shù)據(jù)處理。 這種編寫代碼方式與在Java SE 8之前處理集合的方式有很大不同。但是,它有許多好處。 首先,Streams API利用多種技術(如懶惰和短路)來優(yōu)化您的數(shù)據(jù)處理查詢。 其次,流可以自動并行化以利用多核架構。 在下一篇文章中,我們將探索更高級的操作,例如flatMap和collect。
本章節(jié)代碼請參見stream example
