【編者按】在之前文章中拯辙,我們介紹了 Java 8和Scala的Lambda表達式對比灵迫。在本文,將進行 Hussachai Puripunpinyo Java 和 Scala 對比三部曲的第二部分聘裁,主要關注 Stream 和 Collection赴叹,本文由 OneAPM 工程師編譯整理。
首先兵扬,為大家做一個簡短的介紹麻裳,collection 是有限的數(shù)據(jù)集,而 stream 是數(shù)據(jù)的序列集器钟,可以是有限的或無限的津坑。
Streams API 是 Java 8 中新發(fā)布的 API,主要用于操作 collection 和 streaming 數(shù)據(jù)傲霸。Collections API 會改變數(shù)據(jù)集狀態(tài)疆瑰,而 Streams API 則不會。例如昙啄,當你調(diào)用Collections.sort(list)時穆役,該方法會對傳入的參數(shù)進行排序,而調(diào)用list.stream().sorted() 則會復制一份數(shù)據(jù)進行操作梳凛,保持原數(shù)據(jù)不變耿币。你可以在這里獲得更多關于 API 數(shù)據(jù)流的信息
以下是筆者從 Java 8 文檔中摘出的 collections 和 streams 之間的比較。強烈建議大家閱讀 完整版韧拒。
Streams 和 collections 有以下幾點區(qū)別:
- 無存儲淹接。steam 不是存儲數(shù)據(jù)元素的數(shù)據(jù)結構。而是通過計算操作管道從源頭傳輸數(shù)據(jù)元素叛溢。
2.本質(zhì)是函數(shù)塑悼。對 Stream 對象操作能得到一個結果,但是不會修改原始數(shù)據(jù)楷掉。
Laziness-seeking(延遲搜索):Stream 的很多操作如 filter厢蒜、map、sort 和 duplicate removal(去重)可以延遲實現(xiàn)靖诗,意思是我們只要檢查到滿足要求的元素就可以返回郭怪。
可能是不受限制的:Streams 允許 Client 取足夠多的元素直到滿足某個條件為止支示。而 Collections 不能這么做刊橘。
消耗的。Steam 中的元素在 steam 生存期內(nèi)只能被訪問一次颂鸿。
Java 和 Scala 都可以很簡單地同時計算 collection 中的值促绵。在 Java 中,你只需調(diào)用parallelStream()* 或者 stream().parallel(),而不是stream()败晴。在 Scala 中浓冒,在調(diào)用其他方法之前,必須先調(diào)用 par()函數(shù)尖坤。而且可以通過添加 parallelism 來提高程序的性能稳懒。不幸的是,大多數(shù)時間它的執(zhí)行速度都非常慢慢味。事實上场梆,parallelism 是一個很容易被誤用的功能。 點這閱讀這有趣的文章
在 JavaDoc 中纯路, parallelStream()方法的介紹是:可能返回一個并行的 stream(collection作為數(shù)據(jù)源)或油,所以它也可能返回一個串行 stream。( 有人做過關于該API的研究)
圖像標題
Java 的 Stream API 是延后執(zhí)行的驰唬。這意味著顶岸,沒有指定一個終結操作(比如 collect() 方法調(diào)用),那么所有的中間調(diào)用(比如 filter 調(diào)用)是不會被執(zhí)行的叫编。延遲的流處理主要是為了優(yōu)化 stream API 的執(zhí)行效率辖佣。比如對一個數(shù)據(jù)流進行過濾、映射以及求和運算宵溅,通過使用延后機制凌简,那么所有操作只要遍歷一次,從而減少中間調(diào)用恃逻。同時雏搂,延后執(zhí)行允許每個操作只處理必要的數(shù)據(jù)。相反寇损,Scala 的 collections 是即時處理的凸郑。這樣是否意味著,在測試中矛市,Java Stream API始終優(yōu)于 Scala 芙沥?如果只比較 Java 的 Stream API 和 Scala的 Collection API,那么Java Stream API 的確優(yōu)于 Scala Collection API浊吏。但在 Scala 中有更多的選擇而昨。通過簡單地調(diào)用toStream(),就可以將一個 Collection 轉(zhuǎn)換成一個 Stream找田,或者可以使用 view (一種提供延后處理能力的 Collection)來處理數(shù)據(jù)集合歌憨。
下面粗略介紹下 Scala 的 Stream 和 View 特性
Scala 的 Stream
Scala 的 Stream 和 Java 的有所不同。在 Scala Stream 中墩衙,無需調(diào)用終結操作去取得Stream 的結果务嫡。Stream 是一個繼承 Abstractseq甲抖、 Linearseq和 GenericTraversableTemplate trait的抽象類。所以心铃,你可以把Stream當作 SEQ准谚。
如果你不熟悉 Scala,可以將 Seq 當作 Java 里的 List去扣。(Scala 中的 List 不是一個接口)柱衔。
這里需知道 Streams 中的元素都是延遲計算的,正因為此愉棱,Stream能夠計算無限數(shù)據(jù)流秀存。如果要計算集合中的所有元素,Stream 和 List 有相同的性能羽氮。一旦計算出結果或链,數(shù)值將被緩存。 Stream 有一個 force 函數(shù)档押,能夠強制評估 stream 再返回結果澳盐。注意,不要在無限流中調(diào)用該函數(shù)令宿,也不要強制該 API 處理整個 stream 的操作叼耙,比如 size()、tolist()粒没、foreach() 等筛婉,這些操作在 Scala 的 Stream 中都是隱式的。
在 Scala Stream 中實現(xiàn) Fibonacci 數(shù)列癞松。
def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b)
val fib1 = fibFrom(0, 1) //0 1 1 2 3 5 8 …
val fib5 = fibFrom(0, 5) //0 5 5 10 15 …
//fib1.force //Don’t do this cause it will call the function infinitely and soon you will get the OutOfMemoryError
//fib1.size //Don’t do this too with the same reason as above.
fib1.take(10) //Do this. It will take the first 10 from the inifite Stream.
fib1.take(20).foreach(println(_)) //Prints 20 first numbers
::
是 collection 中常用的連接數(shù)據(jù)的方法爽撒。而 #::
表示是連接數(shù)據(jù)但是是延遲執(zhí)行的(Scala中的方法名都很隨意)。
Scala 的 View
再次重申响蓉,Scala 的 collection 是一個嚴格 collection硕勿,而 view 是非嚴格的。View 是基于一個基礎 collection 的 collection枫甲,其中所有的轉(zhuǎn)換都會延遲執(zhí)行源武。通過調(diào)用 view 函數(shù)可以將嚴格 collection 轉(zhuǎn)換成 view,也可以通過調(diào)用 force 方法轉(zhuǎn)換回來想幻。View 并不緩存結果粱栖,每次調(diào)用時才會執(zhí)行轉(zhuǎn)換。就像數(shù)據(jù)庫的 View脏毯,但它是虛擬 collection闹究。
創(chuàng)建一個數(shù)據(jù)集。
public class Pet {
public static enum Type {
CAT, DOG
}
public static enum Color {
BLACK, WHITE, BROWN, GREEN
}
private String name;
private Type type;
private LocalDate birthdate;
private Color color;
private int weight;
...
}
假設有一個寵物集抄沮,接下來會利用該集合詳細說明跋核。
過濾器
要求:從集合過濾一只胖乎乎的寵物,胖乎乎的定義是體重超過 50 磅叛买,還想得到一個在 2013年1月1日出生的寵物名單砂代。下面的代碼片段顯示了如何以不同的方式實現(xiàn)該濾波器的工作。
Java 方法1:傳統(tǒng)方式
//Before Java 8
List<Pet> tmpList = new ArrayList<>();
for(Pet pet: pets){
if(pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1))
&& pet.getWeight() > 50){
tmpList.add(pet);
}
}
這種方式在命令式語言中十分常見率挣。首先刻伊,必須創(chuàng)建一個臨時集合,然后遍歷所有元素椒功,存儲滿足條件的元素到臨時集中捶箱。的確有點繞口,但其結果和效率都非常不錯动漾。但本人不得不掃興地說丁屎,傳統(tǒng)方法比 Streams API 更快。不過旱眯,完全不用擔心性能問題晨川,因為代碼的簡潔比輕微的性能增益更重要。
Java 方法2:Streams API
//Java 8 - Stream
pets.stream()
.filter(pet -> pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1)))
.filter(pet -> pet.getWeight() > 50)
.collect(toList())
以上代碼表示删豺,使用 Streams API 過濾集合中的元素共虑。之所以故意兩次調(diào)用過濾函數(shù),是想表明 Streams 的 API 設計就像一個 Builder pattern呀页。在 Builder pattern 調(diào)用構建方法之前妈拌,可以將各種方法串聯(lián)起來。在 Streams API 中蓬蝶,構建方法被稱為終結操作尘分,非終結操作的叫做中間操作。終結操作可能不同于構造函數(shù)丸氛,因為它在 Streams API 中只能被調(diào)用一次音诫。但還有很多可使用的終結操作,比如 collect雪位、count竭钝、min、max雹洗、iterator香罐、toArray。這些操作會產(chǎn)生結果时肿,而終端操作會消耗值庇茫,例如 forEach。那么螃成,你認為傳統(tǒng)方法和 Streams API 哪一個的可讀性更強旦签?
Java 方法3:Collections API
//Java 8 - Collection
pets.removeIf(pet -> !(pet.getBirthdate().isBefore(LocalDate.of(2013,Month.JANUARY, 1))
&& pet.getWeight() > 50));
//Applying De-Morgan's law.
pets.removeIf(pet -> pets.get(0).getBirthdate().toEpochDay() >= LocalDate.of(2013, Month.JANUARY, 1).toEpochDay()
|| pet.getWeight() <= 50);
這種方法是最簡短的查坪。但是,它修改了原始集合宁炫,而前面的方法不會偿曙。removeif 函數(shù)將Predicate<T>(函數(shù)接口)作為參數(shù)。Predicate 是一個行為參數(shù)羔巢,它只有一個名為 test 抽象方法望忆,只需要一個對象并返回布爾值。注意竿秆,這里必須使用“启摄!”取反,或者可以應用 De Morgan 定理幽钢,使得代碼看起來像二次聲明歉备。
Scala 方法:Collection、View和Stream
//Scala - strict collection
pets.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //List[Pet]
//Scala - non-strict collection
pets.views.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //SeqView[Pet]
//Scala - stream
pets.toStream.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //Stream[Pet]
Scala 的解決方案類似于 Java 的 Streams API匪燕。但首先威创,必須調(diào)用 view 函數(shù)把嚴格集轉(zhuǎn)向非嚴格集,然后再用 tostream 函數(shù)把嚴格集轉(zhuǎn)成一個 stream谎懦。
接下來直接上代碼肚豺。
分組
通過元素的一個屬性對起所在集合做 group。結果是 Map<T, List<T>>界拦,其中T是一個泛型類型吸申。
要求:通過類型對寵物分組,諸如狗享甸,貓等等截碴。
注意:groupingBy 是 java.util.stream.Collectors 的靜態(tài)的 helper method。
排序
根據(jù)屬性對集合中的元素排序蛉威。結果會是任何類型的集合日丹,根據(jù)配置來維持元素順序。
要求:需按照類型蚯嫌、名字和顏色排序哲虾。
映射
將給定函數(shù)應用在集合元素中。根據(jù)定義的函數(shù)不同择示,其返回的結果類型也不同束凑。
要求:需將寵物轉(zhuǎn)化成字符串,以%s?—?name: %s, color: %s
的格式栅盲。
尋找第一個
返回第一個能與指定 predicate 匹配的值汪诉。
要求:找一個名為Handsome
的寵物。無論有多少個Handsome
谈秫,只取第一個扒寄。
這個問題有點棘手鱼鼓。不知道你是否注意,在 Scala 中筆者所使用的是 find 函數(shù)而不是 filter 该编?如果用 filter 代替 find迄本,它就會計算集合中所有元素,因為 scala collection 是嚴格的上渴。但是,在 Java 的 Streams API 中你可以放心使用 filter喜颁,因為它會計算需要的第一個值稠氮,并不會計算所有元素。這就是延遲執(zhí)行的好處!
接下來半开,向大家介紹 scala 中更多集合延遲執(zhí)行的實例隔披。我們假定 filter 總是返回 true,然后再取第二個值寂拆。將會是什么結果呢奢米?
pets.filter { x => println(x.getName); true }.get(1) --- (1)
pets.toStream.filter { x => println(x.getName); true }.get(1) -- (2)
如上所示,(1)式將會打印出集合中所有寵物的名字纠永,而(2)式則只輸出前2個寵物的名字鬓长。這就是 lazy collection 的好處,總是延遲計算尝江。
pets.view.filter { x => println(x.getName); true }.get(1) --- (3)
(3)式和(2)式會有一樣的結果嗎涉波?錯!它的結果和(1)是一樣的炭序,你知道為什么嗎啤覆?
通過比較 Java 和 Scala 中的一些共同的操作方法 ——filter、group惭聂、map 和 find窗声;很明顯 Scala 的方法比 Java 更簡潔。你更喜歡哪一個呢?哪一個的可讀性更強辜纲?
在文章的下一個部分笨觅,我們將比較哪種方式更快。敬請期待耕腾!
原文鏈接: https://dzone.com/articles/java-8-vs-scalapart-ii-streams-api
OneAPM for Java 能夠深入到所有 Java 應用內(nèi)部完成應用性能管理和監(jiān)控屋摇,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯幽邓、真實用戶體驗監(jiān)控炮温、服務器監(jiān)控和端到端的應用性能管理。想閱讀更多技術文章牵舵,請訪問 OneAPM 官方博客柒啤。