幾乎每一個Java應用都要建立和處理集合。集合對於不少編程任務來講是一個很基本的需求。舉個例子,在銀行交易系統中你須要建立一個集合來存儲用戶的交易請求,而後你須要遍歷整個集合才能找到這個客戶這段時間總共花費了多少金額。儘管集合很是重要,可是在java中對集合的操做並不完美。java
首先,對一個集合處理的模式應該像執行SQL語言操做同樣能夠進行好比查詢(一行交易中最大的一筆)、分組(用於消費平常用品總金額)這樣的操做。大多數據庫也是能夠有明確的相關操做指令,好比"SELECT id, MAX(value) from transactions"SQL查詢語句可讓你找到全部交易中最大的一筆交易和其ID。數據庫
正如你所看到的,咱們不須要去實現怎樣計算最大值(好比循環和變量跟蹤獲得最大值)。咱們只須要表達咱們期待什麼。那麼爲何咱們不能實現與數據庫查詢方式類似的方式來設計實現集合呢?編程
其次,咱們應該怎麼有效處理很大數據量的集合呢?要加速處理的理想方式是採用多核架構CPU,可是編寫並行代碼很難並且會出錯。數組
Java 8 將可以完美解決這這個問題!Stream
的設計可讓你經過陳述式的方式來處理數據。stream還能讓你不寫多線程代碼也是可使用多核架構。聽起來很棒不是嗎?這將是這系列文章將要探索的主要內容。微信
在咱們探索咱們怎麼樣使用stream以前,咱們先看一個使用Java 8 Stream的新的編程模式。咱們須要找出全部銀行交易中類型是grocery
的,而且以交易金額的降序的方式返回交易ID。在Java 7中咱們須要這樣實現:網絡
List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
複製代碼
在Java 8中這樣就能夠實現:數據結構
List<Integer> transactionsIds =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
複製代碼
下圖展現了Java 8的實現代碼,首先,咱們使用stream()
函數從一個交易明細列表中獲取一個stream
對象。接下來是一些操做(filter
,sorted
,map
,collect
)鏈接在一塊兒造成了一個管道,管道能夠被看作是相似數據庫查詢數據的一種方式。多線程
那麼怎麼處理並行代碼呢?在Java8中很是簡單:只須要使用parallelStream()
取代stream()
就能夠了,以下面所示,Stream API將在內部將你的查詢條件分解應用到多核上。架構
List<Integer> transactionsIds =
transactions.parallelStream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
複製代碼
你能夠把stream看作是一種對集合數據提升效能、提供像SQL操做同樣的抽象概念,這個像SQL同樣的操做可使用lambda表達式表示。app
在這一系列關於Java 8 Stream文章的結尾,你將會使用Stream API寫相似於上述代碼來實現強大的查詢功能。
咱們先以一些理論做爲開始。stream的定義是什麼?一個簡單的定義是:"對一個源中的一系列元素進行聚合操做。"把概念拆分一下:
一系列元素:Stream對一組有特定類型的元素提供了一個接口。可是Stream並不真正存儲元素,元素根據需求被計算出結果。
源:Stream能夠處理任何一種數據提供源,好比結合、數組,或者I/O資源。
聚合操做:Stream支持相似SQL同樣的操做,常規的操做都是函數式編程語言,好比filter,map,reduce,find,match,sorted,等等。
Stream操做還具有兩個基本特性使它與集合操做不一樣:
管道:許多Stream操做會返回一個stream對象自己。這就容許全部操做能夠鏈接起來造成一個更大的管道。這就就能夠進行特定的優化了,好比懶加載和短迴路,咱們將在下面介紹。
內部迭代:和集合的顯式迭代(外部迭代)相比,Stream操做不須要咱們手動進行迭代。
讓咱們再次看一下以前的代碼的一些細節:
咱們首先經過stream()
函數從一個交易列表中獲取一個Stream對象。這個數據源是一個交易的列表,將會爲Stream提供一系列元素。接下來,咱們對Stream對象應用一些列的聚合操:filter
(經過給定一個謂詞來過濾元素),sorted
(經過給定一個比較器實現排序),和map
(用於提取信息)。除了collect
其餘操做都會返回Stream,這樣就能夠造成一個管道將它們鏈接起來,咱們能夠把這個鏈看作是一個對源的查詢條件。
在collect被調用以前其實什麼實質性的東西都都沒有被調用。 collect被調用後將會開始處理管道,最終返回結果(結果是一個list)。
在咱們探討stream的各類操做前,咱們仍是看一個stream和collection的概念層面的不一樣之處吧。
Collection和Stream都對一些列元素提供了一些接口。他們的不一樣之處是:Collection是和數據相關的,Stream是和計算相關的。
想一下存在DVD中的電影,這是一個collection,由於他包含了全部的數據結構。然而網絡上的電影是一種流數據。流媒體播放器只須要在用戶觀看前先下載一些幀就能夠觀看了,沒必要全都下載下來。
簡單點說,Collection是一個內存中的數據結構,Collection包括數據結構中的全部值——每一個Collection中的元素在它被添加到集合中以前已經被計算出來了。相反,Stream是一種當須要的時候纔會被計算的數據結構。
使用Collection接口須要用戶作迭代(好比使用foreach),這種方式叫外部迭代。相反,Stream使用的是內部迭代——它會本身爲你作好迭代,而且幫助作好排序。你只須要提供一個函數說明你想要幹什麼。下面代碼使用Collection作外部迭代:
List<String> transactionIds = new ArrayList<>();
for(Transaction t: transactions){
transactionIds.add(t.getId());
}
複製代碼
下面代碼使用Stream作內部迭代
List<Integer> transactionIds =
transactions.stream()
.map(Transaction::getId)
.collect(toList());
複製代碼
Stream 接口定義了許多操做,能夠被分爲兩類。
filter,sorted,和map,這些能夠鏈接起來造成一個管道的操做
collect,能夠關閉管道返回結果的操做
能夠被鏈接起來的操做叫作中間操做
。你能夠把他們鏈接起來,由於他們返回都類型都是Stream。關閉管道的操做叫作終結操做
。他們能夠從管道中產生一個結果,好比一個List,一個Integer,甚至一個void。
中間操做其實不執行任何處理直到一個終結操做被調用;他們很「懶」。由於終結操做一般能夠被合併,而且被終結操做一次性執行。
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;
})
.map(n -> {
System.out.println("mapping " + n);
return n * n;
})
.limit(2)
.collect(toList());
複製代碼
上面的代碼會計算集合中的前兩個偶數,執行結果以下:
filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4
複製代碼
這是由於limit(2)使用了短迴路;咱們只須要處理stream的一部分,而後並返回結果。這就像要計算一個很大的Boollean表達式:只要一個表達式返回false,咱們就能夠判定這個表達式將會返回false而不須要計算全部。這裏limit操做返回一個大小爲2的stream。還有就是filter操做和map操做合併起來一塊兒傳給給了stream。
總結一下咱們現已經已經學到的東西:Stream的操做包括以下三個東西:
Stream提供的操做可分爲以下四類:
過濾:有以下幾種能夠過濾操做
filter(Predicate)
:使用一個謂詞java.util.function.Predicate
做爲參數,返回一個知足謂詞條件的stream。distinct
:返回一個沒有重複元素的stream(根據equals的實現)limit(n)
: 返回一個不超過給定長度的streamskip(n)
: 返回一個忽略前n個的stream查找和匹配:一個一般的數據處理模式是判斷一些元素是否知足給定的屬性。可使用 anyMatch
, allMatch
, 和 noneMatch
操做來幫助你實現。他們都須要一個predicate
做爲參數,而且返回一個boolean做爲做爲結果(所以他們是終結操做)。好比,你可使用allMatch來檢車在Stream中的全部元素是否有一個值大於100,像下面代碼中表示的那樣。
boolean expensive =
transactions.stream()
.allMatch(t -> t.getValue() > 100);
複製代碼
另外,Stream提供了findFirst
和findAny
,能夠從Stream中獲取任意元素。它們能夠和Stream的其餘操做鏈接在一塊兒,好比filter
。findFirst和findAny都返回一個Optional對象,像下面這樣:
Optional<Transaction> =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny();
複製代碼
Optional<T>
類能夠存放一個存在或者不存在的值。在下面代碼中,findAny可能沒有返回一個交易類型是grocery類的信息。Optional存在好多方法檢測元素是否存在。好比,若是一個交易信息存在,咱們可使用相關函數處理optional對象。
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny()
.ifPresent(System.out::println);
複製代碼
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
List<Integer> wordLengths =
words.stream()
.map(String::length)
.collect(toList());
複製代碼
你能夠定製更加複雜的查詢,好比「交易中最大值的id」或者「計算交易金額總和」。這種處理須要使用reduce
操做,reduce能夠將一個操做應用到每一個元素上,知道輸出結果。reduce也常常被叫作摺疊操做,由於你能夠看到這種操做像把一個長的紙張(你的stream)不停地摺疊直到想成一個小方格,這就是摺疊操做。
看一下一個例子:
int sum = 0;
for (int x : numbers) {
sum += x;
}
複製代碼
列表中的每一個元素使用加號都迭代地進行告終合,從而產生告終果。咱們本質上是「減小」了集合中的數據,最終變成了一個數。上面的代碼有兩個參數:初始值和結合list中元素的操做符「+」
當使用Stream的reduce方法時,咱們可使用下面的代碼將集合中的數字元素加起來。reduce方法有兩個參數:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
複製代碼
reduce方法本質上抽象了重複的模式。其餘查詢好比「計算產品」或者「計算最大值」是reduce方法的常規使用場景。
你已經看到了你可使用reduce方法來計算一個Integer的Stream了。然而,咱們卻執行了不少次的開箱操做去重複地把一個Integer對象添加到另外一個上。若是咱們調用sum
方法豈不是很好?像下面代碼那樣,這樣代碼的意圖也更加明確。
int statement =
transactions.stream()
.map(Transaction::getValue)
.sum(); // 這裏是會報錯的
複製代碼
在Java 8 中引入了三種原始的特定數值型Stream接口來解決這個問題,它們是IntStream
, DoubleStream
, 和 LongStream
。它們各自能夠數值型Stream變成一個int、double、long。
可使用mapToInt, mapToDouble, and mapToLong將通用Stream轉化成一個數值型Stream,咱們能夠將上面代碼改爲下面代碼。固然你可使用通用Stream類型取代數值型Stream,而後使用開箱操做。
int statementSum =
transactions.stream()
.mapToInt(Transaction::getValue)
.sum(); // 能夠正確運行
複製代碼
數值類型Stream的另外一個用途就是獲取一個區間的數。好比你可能想要生成1到100以前的全部數。Java 8在IntStream, DoubleStream, 和 LongStream 中引入了兩個靜態方法來幫助生成一個區間,它們是range
和 rangeClosed
.
這兩個方法以區間開始的數爲第一個參數,以區間結束的數爲第二個參數。可是range的區間是開區間的,rangeClosed是閉區間的。下面是一個使用rangeClosed返回10到30之間的奇數的stream。
IntStream oddNumbers =
IntStream.rangeClosed(10, 30)
.filter(n -> n % 2 == 1);
複製代碼
有幾種方式能夠建立Stream。你已經知道了能夠從一個集合中獲取一個Stream,還你使用過數值類型Stream。你可使用數值、數組或者文件建立一個Stream。另外,你甚至可使用一個函數生成一個無窮盡的Stream。
經過數值或者數組建立Stream能夠很直接:對於數值是要使用靜態方法Stream .of,對於數組使用靜態方法Arrays.stream ,像下面代碼這樣:
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
複製代碼
你可使用Files.lines靜態方法將一個文件轉化爲一個Stream。好比,下面代碼計算一個文件的行數。
long numberOfLines =
Files.lines(Paths.get(「yourFile.txt」), Charset.defaultCharset())
.count();
複製代碼
到如今爲止你知道了Stream元素是根據需求產生的。有兩個靜態方法Stream.iterate
和Stream.generate
可讓你從從一個函數中建立一個Stream,由於元素是根據需求計出來的,這兩個方法能夠一直產生元素。這也是咱們叫無窮Stream的緣由:Stream沒有一個固定的大小,可是它和從固定大小的集合中建立的stream是同樣的。
下面代碼是一個使用iterate
建立了包含一個10的倍數的Stream。iterate的第一個參數是初始值,第二個至是用於產生每一個元素的lambda表達式(類型是UnaryOperator)。
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
複製代碼
咱們可使用limit
操做將一個無窮的Stream轉化爲一個大小固定的stream,像下面這樣:
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
複製代碼
Java 8引入了Stream API,這可讓你實現複雜的數據查詢處理。在這片文章中,咱們已經看到了Stream支持不少操做,好比filter、mpa,reduce和iterate,這些操做能夠方便咱們寫簡潔的代碼和實現複雜的數據處理查詢。這和Java 8以前使用的集合有很大的不一樣。Stream有不少好處。首先,Stream API使用了注入懶加載和短迴路的技術優化了數據處理查詢。第二,Stream能夠自動地並行運行,充分使用多核架構。在下一篇文章中,咱們將探討更多高級操做,好比flatMap和collect,請持續關注。
感謝閱讀,有興趣能夠關注微信公衆帳號獲取最新推送文章。