Java SE 8中的主要新語(yǔ)言功能是lambda表達(dá)式。您可以將lambda表達(dá)式視為匿名方法;類(lèi)似方法,lambdas鍵入了參數(shù),一個(gè)body和一個(gè)返回類(lèi)型。但是真正的消息并不是lambda表達(dá)自己,而是它們能夠?qū)崿F(xiàn)的。Lambdas可以輕松地將行為表達(dá)為數(shù)據(jù),從而可以開(kāi)發(fā)更具表現(xiàn)力和更強(qiáng)大的庫(kù)。
在Java SE 8中也引入了一個(gè)這樣的庫(kù),它是一個(gè)java.util.stream包(Streams),它能夠在各種數(shù)據(jù)源上簡(jiǎn)化和聲明性地表達(dá)可能的并行批量操作。像Streams這樣的庫(kù)可能已經(jīng)在早期版本的Java中編寫(xiě)過(guò),但是沒(méi)有一個(gè)緊湊的行為數(shù)據(jù)成語(yǔ),他們使用起來(lái)真的很麻煩,沒(méi)有人會(huì)想使用它們。您可以將Streams視為第一個(gè)利用Java中l(wèi)ambda表達(dá)式的功能的庫(kù),但它并沒(méi)有什么神奇的東西(盡管它被緊密集成到核心的JDK庫(kù)中)。流不是語(yǔ)言的一部分 - 它'
關(guān)于本系列
通過(guò)該java.util.stream軟件包,您可以簡(jiǎn)潔和聲明性地對(duì)集合,數(shù)組和其他數(shù)據(jù)源表示可能的并行批量操作。在Java語(yǔ)言建筑師Brian Goetz的這個(gè)系列中,全面了解Streams庫(kù),并學(xué)習(xí)如何使用它來(lái)獲得最佳的優(yōu)勢(shì)。
本文是系列中第一個(gè)java.util.stream深入探索圖書(shū)館的文章。本部分將向您介紹圖書(shū)館,并概述其優(yōu)勢(shì)和設(shè)計(jì)原則。在后續(xù)的部分中,您將學(xué)習(xí)如何使用流來(lái)匯總和匯總數(shù)據(jù),并查看庫(kù)的內(nèi)部和性能優(yōu)化。
使用流查詢(xún)
流的最常見(jiàn)用途之一是表示對(duì)集合中的數(shù)據(jù)的查詢(xún)。清單1顯示了一個(gè)簡(jiǎn)單流管道的示例。管道收集了買(mǎi)方和賣(mài)方之間購(gòu)買(mǎi)建模的交易,并計(jì)算了居住在紐約的賣(mài)家的交易總金額。
清單1.一個(gè)簡(jiǎn)單的流管道
int totalSalesFromNY
= txns.stream()
.filter(t -> t.getSeller().getAddr().getState().equals("NY"))
.mapToInt(t -> t.getAmount())
.sum();
“Streams利用最強(qiáng)大的計(jì)算原理:組合?!?/i>
該filter()業(yè)務(wù)僅從紐約的賣(mài)家選擇交易。的mapToInt()操作選擇所希望的交易的交易金額。終端sum()操作將這些金額加起來(lái)。
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ù)雜的查詢(xún),請(qǐng)考慮“按照名稱(chēng)排序,打印65歲以上買(mǎi)家交易中的賣(mài)家名稱(chēng)”。寫(xiě)這個(gè)查詢(xún)的老式(命令式)方式可能產(chǎn)生類(lèi)似清單2的東西。
清單2.一個(gè)集合的ad-hoc查詢(xún)
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());
雖然這個(gè)查詢(xún)只是比第一個(gè)更復(fù)雜一些,但很顯然,在命令式方法下的結(jié)果代碼的組織和可讀性已經(jīng)開(kāi)始崩潰了。讀者首先看到的不是計(jì)算的起點(diǎn)和終點(diǎn),這是一個(gè)中間結(jié)果的宣告。要閱讀此代碼,您需要在精確地緩沖大量上下文之前,才能確定代碼實(shí)際執(zhí)行的操作。清單3顯示了如何使用Stream重寫(xiě)此查詢(xún)。
清單3.使用Streams表示的清單2中的查詢(xún)
txns.stream()
.filter(t -> t.getBuyer().getAge() >= 65)
.map(Txn::getSeller)
.distinct()
.sorted(comparing(Seller::getName))
.map(Seller::getName)
.forEach(System.out::println);
清單3中的代碼更容易閱讀,因?yàn)橛脩?hù)既不會(huì)因?yàn)椤袄弊兞慷环稚⒆⒁饬Γ瑂ellers而且sorted在讀取代碼時(shí)不必跟蹤大量的上下文;代碼讀取幾乎完全像問(wèn)題語(yǔ)句。更易于讀取的代碼也比較容易出錯(cuò),因?yàn)榫S護(hù)者更有可能首先正確識(shí)別代碼的作用。
像Streams這樣的圖書(shū)館采用的設(shè)計(jì)方法導(dǎo)致了實(shí)際的分離問(wèn)題??蛻?hù)負(fù)責(zé)指定“什么”的計(jì)算,但圖書(shū)館可以控制“如何”。這種分離往往與專(zhuān)門(mén)知識(shí)的分配平行;客戶(hù)端作者通常對(duì)問(wèn)題領(lǐng)域有更好的了解,而圖書(shū)館作家通常在執(zhí)行的算法屬性方面具有更多的專(zhuān)業(yè)知識(shí)。編寫(xiě)庫(kù)的關(guān)鍵推動(dòng)因素是允許這種分離問(wèn)題的能力,就像傳遞數(shù)據(jù)一樣容易地傳遞行為的能力,這反過(guò)來(lái)又使得API能夠描述復(fù)雜計(jì)算的結(jié)構(gòu),
流管道解剖
所有流計(jì)算共享一個(gè)共同的結(jié)構(gòu):它們具有流源,零個(gè)或多個(gè)中間操作和單個(gè)終端操作。流的元素可以是對(duì)象references(Stream),也可以是原始整數(shù)(IntStream),longs(LongStream)或doubles(DoubleStream)。
由于Java程序消耗的大多數(shù)數(shù)據(jù)已經(jīng)存儲(chǔ)在集合中,許多流計(jì)算使用集合作為其源。CollectionJDK中的實(shí)現(xiàn)已經(jīng)被增強(qiáng)為充當(dāng)有效的流源。但是還存在其他可能的流源,例如陣列,生成器函數(shù)或內(nèi)置工廠(chǎng),如數(shù)字范圍,可以編寫(xiě)自定義流適配器,以便任何數(shù)據(jù)源可以充當(dāng)流源。表1顯示了JDK中的一些流生成方法。
表1. JDK中的流源
方法描述
Collection.stream()從集合的元素創(chuàng)建流。
Stream.of(T...)從傳遞給工廠(chǎng)方法的參數(shù)創(chuàng)建一個(gè)流。
Stream.of(T[])從數(shù)組的元素創(chuàng)建一個(gè)流。
Stream.empty()創(chuàng)建一個(gè)空流。
Stream.iterate(T first, BinaryOperator f)創(chuàng)建一個(gè)由序列組成的無(wú)限流first, f(first), f(f(first)), ...
Stream.iterate(T first, Predicate test, BinaryOperator f)(僅限Java 9)類(lèi)似Stream.iterate(T first, BinaryOperator f),除了流在測(cè)試謂詞返回的第一個(gè)元素上終止false。
Stream.generate(Supplier f)從生成函數(shù)創(chuàng)建無(wú)限流。
IntStream.range(lower, upper)創(chuàng)建一個(gè)IntStream組成的元素從下到上,排他。
IntStream.rangeClosed(lower, upper)創(chuàng)建一個(gè)IntStream由下到上的元素,包括元素。
BufferedReader.lines()創(chuàng)建一個(gè)由一行組成的流BufferedReader.
BitSet.stream()創(chuàng)建一個(gè)IntStream由a中的設(shè)置位的索引組成的BitSet。
CharSequence.chars()IntStream在a中創(chuàng)建一個(gè)對(duì)應(yīng)的charsString。
中間操作 - 例如filter()(選擇符合標(biāo)準(zhǔn)的元素)map()(根據(jù)功能轉(zhuǎn)換元素),distinct()(刪除重復(fù)),limit()(截?cái)嗵囟ù笮〉牧鳎?,以及sorted()- 將流轉(zhuǎn)換為另一個(gè)流。一些操作,例如mapToInt(),采用一種類(lèi)型的流并返回不同類(lèi)型的流;清單1的示例以一個(gè)Stream和更晚的切換開(kāi)始IntStream。表2顯示了一些中間流操作。
表2.中間流操作
手術(shù)內(nèi)容
filter(Predicate)流的元素匹配謂詞
map(Function)將所提供的功能應(yīng)用于流的元素的結(jié)果
flatMap(Function>通過(guò)將提供的流承載函數(shù)應(yīng)用于流的元素而產(chǎn)生的流的元素
distinct()流的元素,重復(fù)的元素被刪除
sorted()流的元素,按照自然順序排列
Sorted(Comparator)流的元素,由提供的比較器排序
limit(long)流的元素截短到提供的長(zhǎng)度
skip(long)流的元素,丟棄前N個(gè)元素
takeWhile(Predicate)(僅限Java 9)流的元素在提供的謂詞不是的第一個(gè)元素上截?cái)鄑rue
dropWhile(Predicate)(僅限Java 9)流的元素,丟棄所提供謂詞所在元素的初始段true
中間操作總是懶惰:調(diào)用中間操作只是在流管道中設(shè)置下一個(gè)階段,但不啟動(dòng)任何工作。中級(jí)操作進(jìn)一步分為無(wú)狀態(tài)和有狀態(tài)操作。無(wú)狀態(tài)操作(例如filter()或map())可以獨(dú)立地對(duì)每個(gè)元素進(jìn)行操作,而狀態(tài)操作(例如sorted()或distinct())可以包含影響其他元素的處理的先前看到的元素的狀態(tài)。
當(dāng)執(zhí)行終端操作(例如,縮減(sum()或max()),應(yīng)用程序(forEach())或search(findFirst()))時(shí),數(shù)據(jù)集的處理開(kāi)始。終端操作產(chǎn)生結(jié)果或副作用。執(zhí)行終端操作時(shí),流管道終止,如果要再次遍歷相同的數(shù)據(jù)集,可以設(shè)置新的流管道。表3顯示了一些終端流操作。
表3.終端流操作
手術(shù)描述
forEach(Consumer action)將提供的操作應(yīng)用于流的每個(gè)元素。
toArray()從流的元素創(chuàng)建一個(gè)數(shù)組。
reduce(...)將流的元素聚合為摘要值。
collect(...)將流的元素聚合到匯總結(jié)果容器中。
min(Comparator)根據(jù)比較器返回流的最小元素。
max(Comparator)根據(jù)比較器返回流的最大元素。
count()返回流的大小。
{any,all,none}Match(Predicate)返回流的任何/ all / none是否與提供的謂詞匹配。
findFirst()返回流的第一個(gè)元素(如果存在)。
findAny()返回流的任何元素(如果存在)。
流與收藏
雖然流可以在表面上類(lèi)似于集合 - 您可能會(huì)將這兩者視為包含數(shù)據(jù) - 實(shí)際上它們顯著不同。集合是數(shù)據(jù)結(jié)構(gòu);其主要關(guān)注點(diǎn)是記憶中的數(shù)據(jù)的組織,并且一段時(shí)間內(nèi)仍然存在一個(gè)集合。通??梢詫⒓嫌米髁鞴艿赖脑椿蚰繕?biāo),但流的重點(diǎn)是計(jì)算,而不是數(shù)據(jù)。數(shù)據(jù)來(lái)自其他地方(集合,數(shù)組,生成函數(shù)或I / O通道),并通過(guò)一系列計(jì)算步驟進(jìn)行處理,以產(chǎn)生結(jié)果或副作用,此時(shí)流完成。流不為其處理的元素提供存儲(chǔ),并且流的生命周期更像是一個(gè)時(shí)間點(diǎn) - 調(diào)用終端操作。不同于集合,流也可以是無(wú)限的;limit()相應(yīng)地,一些操作( ,findFirst())是短路的,并且可以在有限計(jì)算的無(wú)限流上操作。
集合和流也在執(zhí)行其操作的方式上有所不同。收藏活動(dòng)是渴望和變異的;當(dāng)remove()在aList上調(diào)用該方法時(shí),在調(diào)用返回后,您將知道列表狀態(tài)已被修改以反映刪除指定的元素。對(duì)于流,只有終端操作是渴望的;其他人都很懶。流操作表示對(duì)其輸入(也是流)的功能轉(zhuǎn)換,而不是數(shù)據(jù)集上的突變操作(過(guò)濾流生成其流是元素是輸入流子集的新流,但不會(huì)從中刪除任何元素資源)。
將流管道表示為一系列功能轉(zhuǎn)換,可實(shí)現(xiàn)幾個(gè)有用的執(zhí)行策略,如懶惰,短路和操作融合。短路使得管道能夠成功終止,而不檢查所有數(shù)據(jù);諸如“找到第一筆交易超過(guò)$ 1,000”之類(lèi)的查詢(xún)不需要在匹配發(fā)現(xiàn)后再檢查任何更多的事務(wù)。操作融合意味著可以對(duì)數(shù)據(jù)單次執(zhí)行多個(gè)操作;在清單1的例子中,
諸如清單1和清單3中的查詢(xún)的強(qiáng)制性版本通常用于實(shí)現(xiàn)中間計(jì)算結(jié)果的集合,例如過(guò)濾或映射的結(jié)果。這些結(jié)果不僅可以使代碼混亂,而且還會(huì)使執(zhí)行錯(cuò)亂。中間集合的實(shí)現(xiàn)僅用于實(shí)現(xiàn)而不是結(jié)果,并且將計(jì)算周期消耗到將中間結(jié)果組織成僅被丟棄的數(shù)據(jù)結(jié)構(gòu)中。
相比之下,流管線(xiàn)將其操作盡可能少地傳遞給數(shù)據(jù),通常是單次通過(guò)。(有條理的中間操作,如排序,可以引入需要多次執(zhí)行的障礙)。流管道的每個(gè)階段都會(huì)根據(jù)需要輕松地生成其元素,計(jì)算元素,并將它們直接饋送到下一個(gè)階段。您不需要集合來(lái)保存過(guò)濾或映射的中間結(jié)果,因此您可以節(jié)省填充(和垃圾收集)中間集合的工作量。而且,遵循“深度第一”而不是“寬度第一”
除了使用流進(jìn)行計(jì)算之外,您可能需要考慮使用流從API方法返回聚合,以前您可能已返回?cái)?shù)組或集合。返回流通常更有效,因?yàn)槟槐貙⑺袛?shù)據(jù)復(fù)制到新的數(shù)組或集合中。回流也往往更靈活;庫(kù)選擇返回的形式可能不是調(diào)用者需要的,而且很容易將流轉(zhuǎn)換為任何集合類(lèi)型。(返回流的主要情況不合適,落后回歸實(shí)物收集更好,
排比
將計(jì)算結(jié)構(gòu)化為功能轉(zhuǎn)換的有益后果是,您可以輕松地在順序和并行執(zhí)行之間切換,同時(shí)對(duì)代碼進(jìn)行最小的更改。流計(jì)算的順序表達(dá)式和相同計(jì)算的并行表達(dá)式幾乎相同。清單4顯示了如何并行執(zhí)行清單1中的查詢(xún)。
清單4.清單1的并行版本
int totalSalesFromNY
= txns.parallelStream()
.filter(t -> t.getSeller().getAddr().getState().equals("NY"))
.mapToInt(t -> t.getAmount())
.sum();
“將流管道表示為一系列功能轉(zhuǎn)換,可實(shí)現(xiàn)幾項(xiàng)有用的執(zhí)行策略,如懶惰,并行,短路和操作融合?!?/i>
第一行對(duì)并行流而不是順序的請(qǐng)求是與清單1的唯一區(qū)別,因?yàn)镾treams庫(kù)有效地從計(jì)算策略的描述和結(jié)構(gòu)中確定執(zhí)行它的策略。以前,并行需要對(duì)代碼進(jìn)行完全重寫(xiě),這不僅是昂貴的,而且通常也是容易出錯(cuò)的,因?yàn)樗a(chǎn)生的并行代碼看起來(lái)不像順序版本。
所有流操作都可以順序或并行執(zhí)行,但請(qǐng)記住并行性不是魔術(shù)性能灰塵。并行執(zhí)行可能比與順序執(zhí)行速度相同或更慢。最好是從順序流開(kāi)始,并且當(dāng)你知道你將獲得加速的優(yōu)勢(shì)時(shí)應(yīng)用并行性。本系列的后續(xù)部分將返回分析用于并行性能的流管線(xiàn)。
精美的打印
因?yàn)镾treams庫(kù)是協(xié)調(diào)計(jì)算,而是執(zhí)行計(jì)算涉及到由客戶(hù)端提供的lambdas的回調(diào),那么這些lambda表達(dá)式可以做的是受到某些限制。違反這些約束可能導(dǎo)致流管道失敗或計(jì)算不正確的結(jié)果。此外,對(duì)于具有副作用的羔羊,在某些情況下,這些副作用的時(shí)間(或存在)可能是令人驚訝的。
大多數(shù)流量操作要求傳遞給它們的羔羊是無(wú)干擾和無(wú)狀態(tài)的。不干擾意味著它們不會(huì)修改流源;無(wú)狀態(tài)意味著他們不會(huì)訪(fǎng)問(wèn)(讀取或?qū)懭耄┰诹鞑僮鞯囊簧锌赡芨淖兊娜魏螤顟B(tài)。為減少操作(例如,計(jì)算諸如摘要數(shù)據(jù)sum,min或max)傳遞給這些操作必須是lambda表達(dá)式關(guān)聯(lián)(或符合類(lèi)似的要求)。
這些要求部分來(lái)自事實(shí),即如果流水線(xiàn)并行執(zhí)行,則流庫(kù)可以訪(fǎng)問(wèn)數(shù)據(jù)源或從多個(gè)線(xiàn)程同時(shí)調(diào)用這些lambdas。需要限制以確保計(jì)算仍然正確。(這些限制也會(huì)導(dǎo)致更直觀(guān),更容易理解的代碼,而不考慮并行性。)您可能會(huì)試圖說(shuō)服自己,您可以忽略這些限制,因?yàn)槟徽J(rèn)為特定的管道將永遠(yuǎn)不會(huì)運(yùn)行平行,但最好是抵制這種誘惑,否則你會(huì)在你的代碼中埋下時(shí)間炸彈。
所有并發(fā)風(fēng)險(xiǎn)的根源是共享的可變狀態(tài)。共享可變狀態(tài)的一個(gè)可能來(lái)源是流源。如果源是傳統(tǒng)的集合ArrayList,則Streams庫(kù)會(huì)假定它在流操作過(guò)程中保持不變。(明確設(shè)計(jì)用于并發(fā)訪(fǎng)問(wèn)的集合,例如ConcurrentHashMap,不受此假設(shè)的影響)。不僅在流操作期間,不干擾要求不包括其他線(xiàn)程的源突變,而是傳遞給流操作本身的lambdas也應(yīng)避免突變來(lái)源。
除了不修改流源之外,傳遞給流操作的lambdas應(yīng)該是無(wú)狀態(tài)的。例如,清單5中的代碼,試圖消除任何前一個(gè)元素的兩倍的元素違反了這個(gè)規(guī)則。
清單5.使用狀態(tài)lambdas的流管道(不要這樣做?。?/p>
HashSet twiceSeen = new HashSet<>();
int[] result
= elements.stream()
.filter(e -> {
twiceSeen.add(e * 2);
return twiceSeen.contains(e);
})
.toArray();
如果并行執(zhí)行,這個(gè)管道將產(chǎn)生不正確的結(jié)果,原因有兩個(gè)。首先,訪(fǎng)問(wèn)該twiceSeen集合是從多個(gè)線(xiàn)程完成的,沒(méi)有任何協(xié)調(diào),因此不是線(xiàn)程安全的。第二,因?yàn)閿?shù)據(jù)被分區(qū),所以不能保證當(dāng)處理給定的元素時(shí),該元素之前的所有元素都已被處理。
這是最好的,如果傳遞給流操作的lambda表達(dá)式是完全無(wú)副作用-也就是說(shuō),他們沒(méi)有任何突變基于堆的狀態(tài)或者在執(zhí)行過(guò)程中執(zhí)行任何I / O。如果他們確實(shí)有副作用,他們有責(zé)任提供任何必要的協(xié)調(diào),以確保這些副作用是線(xiàn)程安全的。
此外,甚至不能保證所有副作用都將被執(zhí)行。例如,在清單6中,庫(kù)可以自由地避免執(zhí)行map()完全傳遞的lambda。因?yàn)樵淳哂幸阎拇笮。詍ap()操作被認(rèn)為是大小保留,并且映射不影響計(jì)算結(jié)果,庫(kù)可以通過(guò)不執(zhí)行映射來(lái)優(yōu)化計(jì)算?。ǔ讼c調(diào)用映射函數(shù)相關(guān)的工作之外,這種優(yōu)化可以將計(jì)算從O(n)轉(zhuǎn)換為O(1)。
清單6.具有可能無(wú)法執(zhí)行的副作用的流管道
int count =
anArrayList.stream()
.map(e -> { System.out.println("Saw " + e); e })
.count();
唯一的情況是你會(huì)注意到這個(gè)優(yōu)化的效果(除了計(jì)算速度快得多),如果lambda被傳遞到map()有副作用 - 在這種情況下,如果這些副作用不會(huì)發(fā)生,你可能會(huì)感到驚訝。能夠進(jìn)行這些優(yōu)化取決于流操作是功能轉(zhuǎn)換的假設(shè)。大多數(shù)時(shí)候,我們喜歡它,當(dāng)圖書(shū)館使我們的代碼運(yùn)行更快,我們沒(méi)有努力。能夠做到這樣優(yōu)化的代價(jià)是,我們必須接受一些限制,我們通過(guò)流淌的行動(dòng)可以做什么,還有一些我們依賴(lài)于副作用。(總體,
第1部分的結(jié)論
該java.util.stream庫(kù)提供了一種簡(jiǎn)單而靈活的方式來(lái)在各種數(shù)據(jù)源(包括集合,數(shù)組,生成函數(shù),范圍或自定義數(shù)據(jù)結(jié)構(gòu))上表達(dá)可能并行的功能性查詢(xún)。一旦你開(kāi)始使用它,你會(huì)被鉤住!在下一期中看的流庫(kù)的最強(qiáng)大的功能之一:聚集。