java8 流式編程

爲何須要流式操做

集合API是Java API中最重要的部分。基本上每個java程序都離不開集合。儘管很重要,可是現有的集合處理在不少方面都沒法知足須要。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定義了許多操做,這些能夠分爲如下兩類:oracle

  • 像filter,sorted和map同樣的能夠被鏈接起來造成一個管道的操做。
  • 像collect,findFirst和allMatch同樣的終止管道並返回數據的操做。

能夠被鏈接起來的操做被稱爲中間操做,它們能被鏈接起來是由於都返回流。中間操做都「很懶」而且能夠被優化。終止一個流管道的操做被叫作結束操做,它們從流管道返回像List,Integer或者甚至是void等非流類型的數據。
下面咱們介紹一下流裏面的一些方法,完整的方法列表能夠在java.util.stream.Stream找到。app

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 來提升性能時有不少個方面須要考慮,包括數據結構劃分的難易程度,計算每一個元素花費的高低,裝箱的難易,數據量的多少和可用核的數量。
相關文章
相關標籤/搜索