閱讀244 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《JAVA8開發指南》使用流式操作

本章中,你將學習到怎樣使用Stream API進行開發。首先,你將會了解Stream API背後的機製,什麼是流以及流的用處。其次,你將學習到一係列的流式操作、流式數據處理模型以及能讓你寫出更複雜數據查詢的流式集合操作。接下來是如何應用流式操作的例子。最後,你將學習到並行流。

為什麼需要流式操作

集合API是Java API中最重要的部分。基本上每一個java程序都離不開集合。盡管很重要,但是現有的集合處理在很多方麵都無法滿足需要。

一個原因是,許多其他的語言或者類庫以聲明的方式來處理特定的數據模型,比如SQL語言,你可以從表中查詢,按條件過濾數據,並且以某種形式將數據分組,而不必需要了解查詢是如何實現的——數據庫幫你做所有的髒活。這樣做的好處是你的代碼很簡潔。很遺憾,Java沒有這種好東西,你需要用控製流程自己實現所有數據查詢的底層的細節。

其次是你如何有效地處理包含大量數據的集合。理想情況下,為了加快處理過程,你會利用多核架構。但是並發程序不太好寫,而且很容易出錯。

Stream API很好的解決了這兩個問題。它抽象出一種叫做流的東西讓你以聲明的方式處理數據,更重要的是,它還實現了多線程:幫你處理底層諸如線程、鎖、條件變量、易變變量等等。

例如,假定你需要過濾出一遝發票找出哪些跟特定消費者相關的,以金額大小排列,再取出這些發票的ID。如果用Stream API,你很容易寫出下麵這種優雅的查詢:


List ids
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.sorted(comparingDouble(Invoice::getAmount))
.map(Invoice::getId)
.collect(Collectors.toList());

本章後麵,你將了解到這些代碼流程的細節。

什麼是流

說了這麼多,到底什麼是流?通俗地講,你可以認為是支持類似數據庫操作的“花哨的迭代器”。技術上講,它是從某個數據源獲得的支持聚合操作的元素序列。下麵著重介紹一下正式的定義:
元素序列
針對特定元素類型的有序集合流提供了一個接口。但是流不會存儲元素,隻會根據要求對其做計算。
數據源
流所用到的數據源來自集合、數組或者I/O。
聚合操作
流支持類似數據庫的操作以及函數式語言的基本操作,比如filter,map,reduce,findFirst,allMatch,sorted等待。

此外,流操作還有兩種額外的基礎屬性根據不同的集合區分:
管道連接
許多流操作返回流本身,這種操作可以串聯成很長的管道,這種方式更加有利於像延遲加載,短路,循環合並等操作。
內部迭代器
不像集合依賴外部迭代器,流操作在內部幫你實現了迭代器。

流操作

流接口在java.util.stream.Stream定義了許多操作,這些可以分為以下兩類:

  • 像filter,sorted和map一樣的可以被連接起來形成一個管道的操作。
  • 像collect,findFirst和allMatch一樣的終止管道並返回數據的操作。

可以被連接起來的操作被稱為中間操作,它們能被連接起來是因為都返回流。中間操作都“很懶”並且可以被優化。終止一個流管道的操作被叫做結束操作,它們從流管道返回像List,Integer或者甚至是void等非流類型的數據。
下麵我們介紹一下流裏麵的一些方法,完整的方法列表可以在java.util.stream.Stream找到。

Filter

有好幾個方法可以用來從流裏麵過濾出元素:
filter
通過傳遞一個預期匹配的對象作為參數並返回一個包含所有匹配到的對象的流。
distinct
返回包含唯一元素的流(唯一性取決於元素相等的實現方式)。
limit
返回一個特定上限的流。
skip
返回一個丟棄前n個元素的流。

List expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000)
.limit(5)
.collect(Collectors.toList());

Matching

匹配是一個判斷是否匹配到給定屬性的普遍的數據處理模式。你可以用anyMatch,allMatch和noneMatch來匹配數據,它們都需要一個預期匹配的對象作為參數並返回一個boolen型的數據。例如,你可以用allMatch來檢查是否所有的發票流裏麵的元素的值都大於1000:

boolean expensive =
invoices.stream()
.allMatch(inv -> inv.getAmount() > 1_000);

Finding

此外,流接口還提供了像findFirst和findAny等從流中取出任意的元素。它們能與像filter方法相連接。findFirst和findAny都返回一個可選對象(我們已經在第一章中討論過)。

Optional =
invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.findAny();

Mapping

流支持映射方法,傳遞一個函數對象作為方法,把流中的元素轉換成另一種類型。這種方法應用於單個元素,將其映射成新元素。
例如,你有可能想用它來提取流中每個元素的信息。下麵這段代碼從一列發票中返回一列ID:

List ids
= invoices.stream()
.map(Invoice::getId)
.collect(Collectors.toList());

Reducing

另一個常用的模式是把數據源中的所有元素結合起來提供單一的值。例如,“計算最高金額的發票” 或者 “計算所有發票的總額”。 這可以應用流中的reduce方法反複應用於每個元素直到返回最後數據。
下麵是reduce模式的例子,能幫你了解如何用for循環來計算一列數據的和:

int sum = 0;
for (int x : numbers) {
sum += x;
}

對一列數據的每一個元素的值反複應用加法運算符獲得結果,最終將一列值減少到一個值。這段代碼用到兩個參數:初始化總和變量,這裏是0;用來結合所有列表裏麵元素的操作方法,這裏是加法操作。
在流上應用reduce方法,可以把流裏麵的所有元素相加,如下:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce方法需要兩個參數:

  • 初始值,這裏是0
  • 一個BinaryOperator方法連接兩個元素產生一個新元素。reduce方法本質上是抽象了重複方法模式。其他查詢像“計算總和” 或者“計算最大值” 都是reduce方法的特殊用例,比如:


int product = numbers.stream().reduce(1, (a, b) -> a * b);
int max = numbers.stream().reduce(Integer.MIN_VALUE,
Integer::max);

Collectors

目前為止你所了解的方法都是返回另一個流或者一個像boolean,int類型的值,或者返回一個可選對象。相比之下,collect方法是一個結束操作,它可以使流裏麵的所有元素聚集到匯總結果。
傳遞給collect方法參數是一個java.util.stream.Collector類型的對象。Collector對象實際上定義了一個如何把流中的元素聚集到最終結果的方法。最開始,工廠方法Collectors.toList()被用來返回一個描述了如何把流轉變成一個List的Collector對象。後來Collectors類又內建了很多相似的collectors變量。例如,你可以用Collectors.groupingBy方法按消費者把發票分組,如下:

Map<Customer, List> customerToInvoices
= invoices.stream().collect(Collectors.group
ingBy(Invoice::getCustomer));

Putting It All Together

下麵是一個手把手的例子你可以練習如何把老式代碼用Stream API重構。下麵代碼的用途是按照特定消費者過濾出的與訓練有關的發票,以金額高低排序,最後提取出最高的前5張發票的ID:

List oracleAndTrainingInvoices = new ArrayList();
List ids = new ArrayList();
List firstFiveIds = new ArrayList();
for(Invoice inv: invoices) {
if(inv.getCustomer() == Customer.ORACLE) {
if(inv.getTitle().contains("Training")) {
oracleAndTrainingInvoices.add(inv);
}
}
}
Collections.sort(oracleAndTrainingInvoices,
new Comparator() {
@Override
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv1.getAmount(), inv2.getA
mount());
}
});
for(Invoice inv: oracleAndTrainingInvoices) {
ids.add(inv.getId());
}
for(int i = 0; i < 5; i++) {
firstFiveIds.add(ids.get(i));
}

接下來,你將用Stream API一步一步地重構這些代碼。首先,你或者注意到代碼中用到了一個中間容器來存儲那些消費者是Customer.ORACLE並且title中含有“Training”字段的發票。這正是應用filter方法的地方:

Stream oracleAndTrainingInvoices
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.filter(inv ->
inv.getTitle().contains("Training"));

接下來,你需要按照數量來把這些發票排序,你可以用新的工具方法Comparator.comparing結合sorted方法來實現:

Stream sortedInvoices
= oracleAndTrainingInvoices.sorted(comparingDou
ble(Invoice::getAmount));

下麵,你需要提取ID,這是map方法的用途:

Stream ids
= sortedInvoices.map(Invoice::getId);

最後,你隻對前5張發票感興趣。你可以用limit方法截取這5張發票。當你整理一下代碼,再用collect方法,最終的代碼如下:

List firstFiveIds
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.filter(inv ->
inv.getTitle().contains("Training"))
.sorted(comparingDouble(Invoice::getAmount))
.map(Invoice::getId)
.limit(5)
.collect(Collectors.toList());

當你觀察一下老式的代碼你會發現每一個本地變量隻被存儲了一次,被下一段代碼用了一次。當用Stream API之後,就完全消除了這個本地變量。

Parallel Streams

Stream API 支持方便的數據並行。換句話說,你可以明確地讓流管道以並行的方式運行而不用關心底層的具體實現。在這背後,Stream API使用了Fork/Join框架充分利用了你機器的多核架構。
你所需要做的無非是用parallelStream()方法替換stream()方法。例如,下麵代碼顯示如何並行地過濾金額高的發票:

List expensiveInvoices
= invoices.parallelStream()
.filter(inv -> inv.getAmount() > 10_000)
.collect(Collectors.toList());

此外,你可以用並行方法將現有的Stream轉換成parallel Stream:

Stream expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000);
List result
= expensiveInvoices.parallel()
.collect(Collectors.toList());

然而,並不是所有的地方都可以用parallel Stream,從性能角度考慮,有幾點你需要注意:
Splittability
parallel streams的內部實現依賴於將數據結構劃分成可以讓不同線程使用的難易程度。像數組這種數據結構很容易劃分,而像鏈表或者文件這種數據結構很難劃分。
Cost per element
越是計算流中單個元素花費的資源最高,應用並行越有意義。
Boxing
如果可能的話盡量用原始數據類型,這樣可以占用更少的內存,也更緩存命中率也更高。
Size
流中元素的數據量越大越好,因為並行的成本會分攤到所有元素,並行節省的時間相對會更多。當然,這也跟單個元素計算的成本相關。
Number of cores
一般來說,核越多越好。
在實踐中,如果你想提高代碼的性能,你應該檢測你代碼的指標。Java Microbenchmark Harness (JMH) 是一個Oracle維護的流行的框架,你可以用它來幫你完成代碼分析檢測。如果不檢測的話,簡單的應用並行,代碼的性能或許更差。

Summary

下麵是本章的重點內容:

  • 流是一列支持聚合操作的來自於不同數據源的元素列表
  • 流有兩種類型的操作方法:中間方法和終結方法
  • 中間方法可以被連接起來形成管道
  • 中間方法包括filter,map,distinct和sorted
  • 終結方法處理流管道並返回一個結果
  • 終結方法包括allMatch,collect和forEach
  • Collectors是一個第應以了如何將流中的元素聚集到最終結果的方法,包括像List和Map一樣的容器
  • 流管道可以被並行地計算
  • 當應用parallel stream 來提高性能時有很多個方麵需要考慮,包括數據結構劃分的難易程度,計算每個元素花費的高低,裝箱的難易,數據量的多少和可用核的數量。
  • 轉載自 並發編程網 - ifeve.com

最後更新:2017-05-19 14:33:31

  上一篇:go  《Jersey用戶指南》翻譯邀請
  下一篇:go  馬斯克的“半機械人”還沒出現,這位日本少年卻已經可以讓你變身“半機械人”了