Java SE 8中的主要新語言功能是lambda表達式。您可以將lambda表達式視為匿名方法;類似方法括眠,lambdas鍵入了參數(shù)哺窄,一個body和一個返回類型捐下。但是真正的消息并不是lambda表達自己,而是它們能夠?qū)崿F(xiàn)的萌业。Lambdas可以輕松地將行為表達為數(shù)據(jù)生年,從而可以開發(fā)更具表現(xiàn)力和更強大的庫抱婉。
在Java SE 8中也引入了一個這樣的庫蒸绩,它是一個java.util.stream包(Streams)患亿,它能夠在各種數(shù)據(jù)源上簡化和聲明性地表達可能的并行批量操作押逼。像Streams這樣的庫可能已經(jīng)在早期版本的Java中編寫過挑格,但是沒有一個緊湊的行為數(shù)據(jù)成語漂彤,他們使用起來真的很麻煩挫望,沒有人會想使用它們确镊。您可以將Streams視為第一個利用Java中l(wèi)ambda表達式的功能的庫蕾域,但它并沒有什么神奇的東西(盡管它被緊密集成到核心的JDK庫中)旨巷。流不是語言的一部分 - 它'
關(guān)于本系列
通過該java.util.stream軟件包采呐,您可以簡潔和聲明性地對集合斧吐,數(shù)組和其他數(shù)據(jù)源表示可能的并行批量操作煤率。在Java語言建筑師Brian Goetz的這個系列中蝶糯,全面了解Streams庫昼捍,并學(xué)習(xí)如何使用它來獲得最佳的優(yōu)勢妒茬。
本文是系列中第一個java.util.stream深入探索圖書館的文章乍钻。本部分將向您介紹圖書館,并概述其優(yōu)勢和設(shè)計原則。在后續(xù)的部分中熬丧,您將學(xué)習(xí)如何使用流來匯總和匯總數(shù)據(jù)析蝴,并查看庫的內(nèi)部和性能優(yōu)化闷畸。
使用流查詢
流的最常見用途之一是表示對集合中的數(shù)據(jù)的查詢佑菩。清單1顯示了一個簡單流管道的示例殿漠。管道收集了買方和賣方之間購買建模的交易绞幌,并計算了居住在紐約的賣家的交易總金額莲蜘。
清單1.一個簡單的流管道
int totalSalesFromNY
= txns.stream()
.filter(t -> t.getSeller().getAddr().getState().equals("NY"))
.mapToInt(t -> t.getAmount())
.sum();
“Streams利用最強大的計算原理:組合票渠∽拢”
該filter()業(yè)務(wù)僅從紐約的賣家選擇交易择诈。的mapToInt()操作選擇所希望的交易的交易金額羞芍。終端sum()操作將這些金額加起來荷科。
Although this example is pretty and easy to read, detractors might point out that the imperative (for-loop) version of this query is also simple and takes fewer lines of code to express. But the problem doesn't have to get much more complicated for the benefits of the stream approach to become evident. Streams exploit that most powerful of computing principles: composition. By composing complex operations out of simple building blocks (filtering, mapping, sorting, aggregation), streams queries are more likely to remain straightforward to write and read as the problem gets complicated than are more ad-hoc computations on the same data sources.
作為與清單1相同域名的更為復(fù)雜的查詢畏浆,請考慮“按照名稱排序刻获,打印65歲以上買家交易中的賣家名稱”蝎毡。寫這個查詢的老式(命令式)方式可能產(chǎn)生類似清單2的東西沐兵。
清單2.一個集合的ad-hoc查詢
Set sellers = new HashSet<>();
for (Txn t : txns) {
if (t.getBuyer().getAge() >= 65)
sellers.add(t.getSeller());
}
List sorted = new ArrayList<>(sellers);
Collections.sort(sorted, new Comparator() {
public int compare(Seller a, Seller b) {
return a.getName().compareTo(b.getName());
}
});
for (Seller s : sorted)
System.out.println(s.getName());
雖然這個查詢只是比第一個更復(fù)雜一些碳想,但很顯然移袍,在命令式方法下的結(jié)果代碼的組織和可讀性已經(jīng)開始崩潰了葡盗。讀者首先看到的不是計算的起點和終點觅够,這是一個中間結(jié)果的宣告喘先。要閱讀此代碼窘拯,您需要在精確地緩沖大量上下文之前涤姊,才能確定代碼實際執(zhí)行的操作思喊。清單3顯示了如何使用Stream重寫此查詢恨课。
清單3.使用Streams表示的清單2中的查詢
txns.stream()
.filter(t -> t.getBuyer().getAge() >= 65)
.map(Txn::getSeller)
.distinct()
.sorted(comparing(Seller::getName))
.map(Seller::getName)
.forEach(System.out::println);
清單3中的代碼更容易閱讀剂公,因為用戶既不會因為“垃圾”變量而被分散注意力纲辽,sellers而且sorted在讀取代碼時不必跟蹤大量的上下文;代碼讀取幾乎完全像問題語句文兑。更易于讀取的代碼也比較容易出錯绿贞,因為維護者更有可能首先正確識別代碼的作用籍铁。
像Streams這樣的圖書館采用的設(shè)計方法導(dǎo)致了實際的分離問題拒名≡鱿裕客戶負責(zé)指定“什么”的計算同云,但圖書館可以控制“如何”。這種分離往往與專門知識的分配平行;客戶端作者通常對問題領(lǐng)域有更好的了解旱易,而圖書館作家通常在執(zhí)行的算法屬性方面具有更多的專業(yè)知識阀坏。編寫庫的關(guān)鍵推動因素是允許這種分離問題的能力全释,就像傳遞數(shù)據(jù)一樣容易地傳遞行為的能力浸船,這反過來又使得API能夠描述復(fù)雜計算的結(jié)構(gòu)李命,
流管道解剖
所有流計算共享一個共同的結(jié)構(gòu):它們具有流源封字,零個或多個中間操作和單個終端操作流妻。流的元素可以是對象references(Stream)绅这,也可以是原始整數(shù)(IntStream)证薇,longs(LongStream)或doubles(DoubleStream)浑度。
由于Java程序消耗的大多數(shù)數(shù)據(jù)已經(jīng)存儲在集合中箩张,許多流計算使用集合作為其源伏钠。CollectionJDK中的實現(xiàn)已經(jīng)被增強為充當(dāng)有效的流源熟掂。但是還存在其他可能的流源赴肚,例如陣列,生成器函數(shù)或內(nèi)置工廠踊跟,如數(shù)字范圍商玫,可以編寫自定義流適配器拳昌,以便任何數(shù)據(jù)源可以充當(dāng)流源炬藤。表1顯示了JDK中的一些流生成方法上真。
表1. JDK中的流源
方法描述
Collection.stream()從集合的元素創(chuàng)建流睡互。
Stream.of(T...)從傳遞給工廠方法的參數(shù)創(chuàng)建一個流。
Stream.of(T[])從數(shù)組的元素創(chuàng)建一個流蠢壹。
Stream.empty()創(chuàng)建一個空流图贸。
Stream.iterate(T first, BinaryOperator f)創(chuàng)建一個由序列組成的無限流first, f(first), f(f(first)), ...
Stream.iterate(T first, Predicate test, BinaryOperator f)(僅限Java 9)類似Stream.iterate(T first, BinaryOperator f),除了流在測試謂詞返回的第一個元素上終止false沟优。
Stream.generate(Supplier f)從生成函數(shù)創(chuàng)建無限流挠阁。
IntStream.range(lower, upper)創(chuàng)建一個IntStream組成的元素從下到上侵俗,排他。
IntStream.rangeClosed(lower, upper)創(chuàng)建一個IntStream由下到上的元素寻歧,包括元素渣玲。
BufferedReader.lines()創(chuàng)建一個由一行組成的流BufferedReader.
BitSet.stream()創(chuàng)建一個IntStream由a中的設(shè)置位的索引組成的BitSet。
CharSequence.chars()IntStream在a中創(chuàng)建一個對應(yīng)的charsString枚钓。
中間操作 - 例如filter()(選擇符合標(biāo)準(zhǔn)的元素)map()(根據(jù)功能轉(zhuǎn)換元素)搀捷,distinct()(刪除重復(fù)),limit()(截斷特定大小的流)家厌,以及sorted()- 將流轉(zhuǎn)換為另一個流饭于。一些操作掰吕,例如mapToInt(),采用一種類型的流并返回不同類型的流;清單1的示例以一個Stream和更晚的切換開始IntStream吗讶。表2顯示了一些中間流操作照皆。
表2.中間流操作
手術(shù)內(nèi)容
filter(Predicate)流的元素匹配謂詞
map(Function)將所提供的功能應(yīng)用于流的元素的結(jié)果
flatMap(Function>通過將提供的流承載函數(shù)應(yīng)用于流的元素而產(chǎn)生的流的元素
distinct()流的元素,重復(fù)的元素被刪除
sorted()流的元素橡疼,按照自然順序排列
Sorted(Comparator)流的元素翔曲,由提供的比較器排序
limit(long)流的元素截短到提供的長度
skip(long)流的元素敌土,丟棄前N個元素
takeWhile(Predicate)(僅限Java 9)流的元素在提供的謂詞不是的第一個元素上截斷true
dropWhile(Predicate)(僅限Java 9)流的元素,丟棄所提供謂詞所在元素的初始段true
中間操作總是懶惰:調(diào)用中間操作只是在流管道中設(shè)置下一個階段矩欠,但不啟動任何工作癌淮。中級操作進一步分為無狀態(tài)和有狀態(tài)操作乳蓄。無狀態(tài)操作(例如filter()或map())可以獨立地對每個元素進行操作匣摘,而狀態(tài)操作(例如sorted()或distinct())可以包含影響其他元素的處理的先前看到的元素的狀態(tài)音榜。
當(dāng)執(zhí)行終端操作(例如赠叼,縮減(sum()或max())嘴办,應(yīng)用程序(forEach())或search(findFirst()))時涧郊,數(shù)據(jù)集的處理開始彤灶。終端操作產(chǎn)生結(jié)果或副作用幌陕。執(zhí)行終端操作時搏熄,流管道終止搬卒,如果要再次遍歷相同的數(shù)據(jù)集契邀,可以設(shè)置新的流管道微饥。表3顯示了一些終端流操作欠橘。
表3.終端流操作
手術(shù)描述
forEach(Consumer action)將提供的操作應(yīng)用于流的每個元素肃续。
toArray()從流的元素創(chuàng)建一個數(shù)組。
reduce(...)將流的元素聚合為摘要值喳逛。
collect(...)將流的元素聚合到匯總結(jié)果容器中瞧捌。
min(Comparator)根據(jù)比較器返回流的最小元素。
max(Comparator)根據(jù)比較器返回流的最大元素润文。
count()返回流的大小姐呐。
{any,all,none}Match(Predicate)返回流的任何/ all / none是否與提供的謂詞匹配。
findFirst()返回流的第一個元素(如果存在)典蝌。
findAny()返回流的任何元素(如果存在)曙砂。
流與收藏
雖然流可以在表面上類似于集合 - 您可能會將這兩者視為包含數(shù)據(jù) - 實際上它們顯著不同骏掀。集合是數(shù)據(jù)結(jié)構(gòu);其主要關(guān)注點是記憶中的數(shù)據(jù)的組織鸠澈,并且一段時間內(nèi)仍然存在一個集合乔夯。通常可以將集合用作流管道的源或目標(biāo)款侵,但流的重點是計算末荐,而不是數(shù)據(jù)。數(shù)據(jù)來自其他地方(集合新锈,數(shù)組甲脏,生成函數(shù)或I / O通道),并通過一系列計算步驟進行處理妹笆,以產(chǎn)生結(jié)果或副作用块请,此時流完成。流不為其處理的元素提供存儲拳缠,并且流的生命周期更像是一個時間點 - 調(diào)用終端操作墩新。不同于集合,流也可以是無限的;limit()相應(yīng)地窟坐,一些操作( ,findFirst())是短路的哲鸳,并且可以在有限計算的無限流上操作臣疑。
集合和流也在執(zhí)行其操作的方式上有所不同。收藏活動是渴望和變異的;當(dāng)remove()在aList上調(diào)用該方法時徙菠,在調(diào)用返回后讯沈,您將知道列表狀態(tài)已被修改以反映刪除指定的元素。對于流婿奔,只有終端操作是渴望的;其他人都很懶缺狠。流操作表示對其輸入(也是流)的功能轉(zhuǎn)換,而不是數(shù)據(jù)集上的突變操作(過濾流生成其流是元素是輸入流子集的新流萍摊,但不會從中刪除任何元素資源)挤茄。
將流管道表示為一系列功能轉(zhuǎn)換,可實現(xiàn)幾個有用的執(zhí)行策略记餐,如懶惰驮樊,短路和操作融合薇正。短路使得管道能夠成功終止片酝,而不檢查所有數(shù)據(jù);諸如“找到第一筆交易超過$ 1,000”之類的查詢不需要在匹配發(fā)現(xiàn)后再檢查任何更多的事務(wù)。操作融合意味著可以對數(shù)據(jù)單次執(zhí)行多個操作;在清單1的例子中挖腰,
諸如清單1和清單3中的查詢的強制性版本通常用于實現(xiàn)中間計算結(jié)果的集合雕沿,例如過濾或映射的結(jié)果。這些結(jié)果不僅可以使代碼混亂猴仑,而且還會使執(zhí)行錯亂审轮。中間集合的實現(xiàn)僅用于實現(xiàn)而不是結(jié)果肥哎,并且將計算周期消耗到將中間結(jié)果組織成僅被丟棄的數(shù)據(jù)結(jié)構(gòu)中。
相比之下疾渣,流管線將其操作盡可能少地傳遞給數(shù)據(jù)篡诽,通常是單次通過。(有條理的中間操作榴捡,如排序杈女,可以引入需要多次執(zhí)行的障礙)。流管道的每個階段都會根據(jù)需要輕松地生成其元素吊圾,計算元素达椰,并將它們直接饋送到下一個階段。您不需要集合來保存過濾或映射的中間結(jié)果项乒,因此您可以節(jié)省填充(和垃圾收集)中間集合的工作量啰劲。而且,遵循“深度第一”而不是“寬度第一”
除了使用流進行計算之外檀何,您可能需要考慮使用流從API方法返回聚合蝇裤,以前您可能已返回數(shù)組或集合。返回流通常更有效频鉴,因為您不必將所有數(shù)據(jù)復(fù)制到新的數(shù)組或集合中猖辫。回流也往往更靈活;庫選擇返回的形式可能不是調(diào)用者需要的砚殿,而且很容易將流轉(zhuǎn)換為任何集合類型啃憎。(返回流的主要情況不合適,落后回歸實物收集更好似炎,
排比
將計算結(jié)構(gòu)化為功能轉(zhuǎn)換的有益后果是辛萍,您可以輕松地在順序和并行執(zhí)行之間切換,同時對代碼進行最小的更改羡藐。流計算的順序表達式和相同計算的并行表達式幾乎相同贩毕。清單4顯示了如何并行執(zhí)行清單1中的查詢。
清單4.清單1的并行版本
int totalSalesFromNY
= txns.parallelStream()
.filter(t -> t.getSeller().getAddr().getState().equals("NY"))
.mapToInt(t -> t.getAmount())
.sum();
“將流管道表示為一系列功能轉(zhuǎn)換仆嗦,可實現(xiàn)幾項有用的執(zhí)行策略辉阶,如懶惰,并行瘩扼,短路和操作融合谆甜。”
第一行對并行流而不是順序的請求是與清單1的唯一區(qū)別集绰,因為Streams庫有效地從計算策略的描述和結(jié)構(gòu)中確定執(zhí)行它的策略规辱。以前,并行需要對代碼進行完全重寫栽燕,這不僅是昂貴的罕袋,而且通常也是容易出錯的改淑,因為所產(chǎn)生的并行代碼看起來不像順序版本。
所有流操作都可以順序或并行執(zhí)行浴讯,但請記住并行性不是魔術(shù)性能灰塵朵夏。并行執(zhí)行可能比與順序執(zhí)行速度相同或更慢。最好是從順序流開始榆纽,并且當(dāng)你知道你將獲得加速的優(yōu)勢時應(yīng)用并行性侍郭。本系列的后續(xù)部分將返回分析用于并行性能的流管線。
精美的打印
因為Streams庫是協(xié)調(diào)計算掠河,而是執(zhí)行計算涉及到由客戶端提供的lambdas的回調(diào)亮元,那么這些lambda表達式可以做的是受到某些限制。違反這些約束可能導(dǎo)致流管道失敗或計算不正確的結(jié)果唠摹。此外爆捞,對于具有副作用的羔羊,在某些情況下勾拉,這些副作用的時間(或存在)可能是令人驚訝的煮甥。
大多數(shù)流量操作要求傳遞給它們的羔羊是無干擾和無狀態(tài)的。不干擾意味著它們不會修改流源;無狀態(tài)意味著他們不會訪問(讀取或?qū)懭耄┰诹鞑僮鞯囊簧锌赡芨淖兊娜魏螤顟B(tài)藕赞。為減少操作(例如,計算諸如摘要數(shù)據(jù)sum斧蜕,min或max)傳遞給這些操作必須是lambda表達式關(guān)聯(lián)(或符合類似的要求)。
這些要求部分來自事實批销,即如果流水線并行執(zhí)行,則流庫可以訪問數(shù)據(jù)源或從多個線程同時調(diào)用這些lambdas均芽。需要限制以確保計算仍然正確。(這些限制也會導(dǎo)致更直觀掀宋,更容易理解的代碼,而不考慮并行性劲妙。)您可能會試圖說服自己,您可以忽略這些限制是趴,因為您不認為特定的管道將永遠不會運行平行澄惊,但最好是抵制這種誘惑富雅,否則你會在你的代碼中埋下時間炸彈。
所有并發(fā)風(fēng)險的根源是共享的可變狀態(tài)肛搬。共享可變狀態(tài)的一個可能來源是流源没佑。如果源是傳統(tǒng)的集合ArrayList,則Streams庫會假定它在流操作過程中保持不變温赔。(明確設(shè)計用于并發(fā)訪問的集合蛤奢,例如ConcurrentHashMap,不受此假設(shè)的影響)陶贼。不僅在流操作期間啤贩,不干擾要求不包括其他線程的源突變,而是傳遞給流操作本身的lambdas也應(yīng)避免突變來源拜秧。
除了不修改流源之外痹屹,傳遞給流操作的lambdas應(yīng)該是無狀態(tài)的。例如枉氮,清單5中的代碼志衍,試圖消除任何前一個元素的兩倍的元素違反了這個規(guī)則。
清單5.使用狀態(tài)lambdas的流管道(不要這樣做A奶妗)
HashSet twiceSeen = new HashSet<>();
int[] result
= elements.stream()
.filter(e -> {
twiceSeen.add(e * 2);
return twiceSeen.contains(e);
})
.toArray();
如果并行執(zhí)行楼肪,這個管道將產(chǎn)生不正確的結(jié)果,原因有兩個惹悄。首先春叫,訪問該twiceSeen集合是從多個線程完成的,沒有任何協(xié)調(diào)泣港,因此不是線程安全的象缀。第二,因為數(shù)據(jù)被分區(qū)爷速,所以不能保證當(dāng)處理給定的元素時央星,該元素之前的所有元素都已被處理。
這是最好的惫东,如果傳遞給流操作的lambda表達式是完全無副作用-也就是說莉给,他們沒有任何突變基于堆的狀態(tài)或者在執(zhí)行過程中執(zhí)行任何I / O廉沮。如果他們確實有副作用,他們有責(zé)任提供任何必要的協(xié)調(diào)叁幢,以確保這些副作用是線程安全的曼玩。
此外,甚至不能保證所有副作用都將被執(zhí)行黍判。例如顷帖,在清單6中,庫可以自由地避免執(zhí)行map()完全傳遞的lambda榴嗅。因為源具有已知的大小录肯,所以map()操作被認為是大小保留吊说,并且映射不影響計算結(jié)果颁井,庫可以通過不執(zhí)行映射來優(yōu)化計算!(除了消除與調(diào)用映射函數(shù)相關(guān)的工作之外养涮,這種優(yōu)化可以將計算從O(n)轉(zhuǎn)換為O(1)贯吓。
清單6.具有可能無法執(zhí)行的副作用的流管道
int count =
anArrayList.stream()
.map(e -> { System.out.println("Saw " + e); e })
.count();
唯一的情況是你會注意到這個優(yōu)化的效果(除了計算速度快得多)悄谐,如果lambda被傳遞到map()有副作用 - 在這種情況下库北,如果這些副作用不會發(fā)生,你可能會感到驚訝。能夠進行這些優(yōu)化取決于流操作是功能轉(zhuǎn)換的假設(shè)垃你。大多數(shù)時候,我們喜歡它皆刺,當(dāng)圖書館使我們的代碼運行更快芹橡,我們沒有努力望伦。能夠做到這樣優(yōu)化的代價是屯伞,我們必須接受一些限制豪直,我們通過流淌的行動可以做什么弓乙,還有一些我們依賴于副作用。(總體勾习,
第1部分的結(jié)論
該java.util.stream庫提供了一種簡單而靈活的方式來在各種數(shù)據(jù)源(包括集合巧婶,數(shù)組涂乌,生成函數(shù)湾盒,范圍或自定義數(shù)據(jù)結(jié)構(gòu))上表達可能并行的功能性查詢。一旦你開始使用它诅需,你會被鉤籽咚分衫!在下一期中看的流庫的最強大的功能之一:聚集。