Java 8 vs. Scala(二):Stream vs. Collection

【編者按】在以前文章中,咱們介紹了 Java 8和Scala的Lambda表達式對比。在本文,將進行 Hussachai Puripunpinyo Java 和 Scala 對比三部曲的第二部分,主要關注 Stream 和 Collection,本文由 OneAPM 工程師編譯整理。html

首先,爲你們作一個簡短的介紹,collection 是有限的數據集,而 stream 是數據的序列集,能夠是有限的或無限的。java

Streams API 是 Java 8 中新發布的 API,主要用於操做 collection 和 streaming 數據。Collections API 會改變數據集狀態,而 Streams API 則不會。例如,當你調用Collections.sort(list)時,該方法會對傳入的參數進行排序,而調用list.stream().sorted() 則會複製一份數據進行操做,保持原數據不變。你能夠在這裏得到更多關於 API 數據流的信息數據庫

如下是筆者從 Java 8 文檔中摘出的 collections 和 streams 之間的比較。強烈建議你們閱讀 完整版api

Streams 和 collections 有如下幾點區別:緩存

  1. 無存儲。steam 不是存儲數據元素的數據結構。而是經過計算操做管道從源頭傳輸數據元素。

2.本質是函數。對 Stream 對象操做能獲得一個結果,可是不會修改原始數據。服務器

  1. Laziness-seeking(延遲搜索):Stream 的不少操做如 filter、map、sort 和 duplicate removal(去重)能夠延遲實現,意思是咱們只要檢查到知足要求的元素就能夠返回。數據結構

  2. 多是不受限制的:Streams 容許 Client 取足夠多的元素直到知足某個條件爲止。而 Collections 不能這麼作。oracle

  3. 消耗的。Steam 中的元素在 steam 生存期內只能被訪問一次。函數

Java 和 Scala 均可以很簡單地同時計算 collection 中的值。在 Java 中,你只需調用parallelStream()* 或者 stream().parallel(),而不是stream()。在 Scala 中,在調用其餘方法以前,必須先調用 par()函數。並且能夠經過添加 parallelism 來提升程序的性能。不幸的是,大多數時間它的執行速度都很是慢。事實上,parallelism 是一個很容易被誤用的功能。 點這閱讀這有趣的文章性能

在 JavaDoc 中, parallelStream()方法的介紹是:可能返回一個並行的 stream(collection做爲數據源),因此它也可能返回一個串行 stream。( 有人作過關於該API的研究

圖像標題

Java 的 Stream API 是延後執行的。這意味着,沒有指定一個終結操做(好比 collect() 方法調用),那麼全部的中間調用(好比 filter 調用)是不會被執行的。延遲的流處理主要是爲了優化 stream API 的執行效率。好比對一個數據流進行過濾、映射以及求和運算,經過使用延後機制,那麼全部操做只要遍歷一次,從而減小中間調用。同時,延後執行容許每一個操做只處理必要的數據。相反,Scala 的 collections 是即時處理的。這樣是否意味着,在測試中,Java Stream API始終優於 Scala ?若是隻比較 Java 的 Stream API 和 Scala的 Collection API,那麼Java Stream API 的確優於 Scala Collection API。但在 Scala 中有更多的選擇。經過簡單地調用toStream(),就能夠將一個 Collection 轉換成一個 Stream,或者可使用 view (一種提供延後處理能力的 Collection)來處理數據集合。

下面粗略介紹下 Scala 的 Stream 和 View 特性

Scala 的 Stream

Scala 的 Stream 和 Java 的有所不一樣。在 Scala Stream 中,無需調用終結操做去取得Stream 的結果。Stream 是一個繼承 AbstractseqLinearseqGenericTraversableTemplate trait的抽象類。因此,你能夠把Stream看成 SEQ

若是你不熟悉 Scala,能夠將 Seq 看成 Java 裏的 List。(Scala 中的 List 不是一個接口)。

這裏需知道 Streams 中的元素都是延遲計算的,正由於此,Stream可以計算無限數據流。若是要計算集合中的全部元素,Stream 和 List 有相同的性能。一旦計算出結果,數值將被緩存。 Stream 有一個 force 函數,可以強制評估 stream 再返回結果。注意,不要在無限流中調用該函數,也不要強制該 API 處理整個 stream 的操做,好比 size()、tolist()、foreach() 等,這些操做在 Scala 的 Stream 中都是隱式的。

在 Scala Stream 中實現 Fibonacci 數列。

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 中經常使用的鏈接數據的方法。而 #:: 表示是鏈接數據可是是延遲執行的(Scala中的方法名都很隨意)。

Scala 的 View

再次重申,Scala 的 collection 是一個嚴格 collection,而 view 是非嚴格的。View 是基於一個基礎 collection 的 collection,其中全部的轉換都會延遲執行。經過調用 view 函數能夠將嚴格 collection 轉換成 view,也能夠經過調用 force 方法轉換回來。View 並不緩存結果,每次調用時纔會執行轉換。就像數據庫的 View,但它是虛擬 collection。

建立一個數據集。

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日出生的寵物名單。下面的代碼片斷顯示瞭如何以不一樣的方式實現該濾波器的工做。

Java 方法1:傳統方式

//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);
    }
}

這種方式在命令式語言中十分常見。首先,必須建立一個臨時集合,而後遍歷全部元素,存儲知足條件的元素到臨時集中。的確有點繞口,但其結果和效率都很是不錯。但本人不得不掃興地說,傳統方法比 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 過濾集合中的元素。之因此故意兩次調用過濾函數,是想代表 Streams 的 API 設計就像一個 Builder pattern。在 Builder pattern 調用構建方法以前,能夠將各類方法串聯起來。在 Streams API 中,構建方法被稱爲終結操做,非終結操做的叫作中間操做。終結操做可能不一樣於構造函數,由於它在 Streams API 中只能被調用一次。但還有不少可以使用的終結操做,好比 collect、count、min、max、iterator、toArray。這些操做會產生結果,而終端操做會消耗值,例如 forEach。那麼,你認爲傳統方法和 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 函數將Predicate<T>(函數接口)做爲參數。Predicate 是一個行爲參數,它只有一個名爲 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。但首先,必須調用 view 函數把嚴格集轉向非嚴格集,而後再用 tostream 函數把嚴格集轉成一個 stream。

接下來直接上代碼。

分組

經過元素的一個屬性對起所在集合作 group。結果是 Map<T, List<T>>,其中T是一個泛型類型。

要求:經過類型對寵物分組,諸如狗,貓等等。

Java 8 vs. Scala(二):Stream vs. Collection

注意:groupingBy 是 java.util.stream.Collectors 的靜態的 helper method。

排序

根據屬性對集合中的元素排序。結果會是任何類型的集合,根據配置來維持元素順序。

要求:需按照類型、名字和顏色排序。

Java 8 vs. Scala(二):Stream vs. Collection

映射

將給定函數應用在集合元素中。根據定義的函數不一樣,其返回的結果類型也不一樣。

要求:需將寵物轉化成字符串,以%s — name: %s, color: %s的格式。

Java 8 vs. Scala(二):Stream vs. Collection

尋找第一個

返回第一個能與指定 predicate 匹配的值。

要求:找一個名爲Handsome的寵物。不管有多少個Handsome,只取第一個。

這個問題有點棘手。不知道你是否注意,在 Scala 中筆者所使用的是 find 函數而不是 filter ?若是用 filter 代替 find,它就會計算集合中全部元素,由於 scala collection 是嚴格的。可是,在 Java 的 Streams API 中你能夠放心使用 filter,由於它會計算須要的第一個值,並不會計算全部元素。這就是延遲執行的好處!

接下來,向你們介紹 scala 中更多集合延遲執行的實例。咱們假定 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 應用內部完成應用性能管理和監控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實用戶體驗監控、服務器監控和端到端的應用性能管理。想閱讀更多技術文章,請訪問 OneAPM 官方博客

相關文章
相關標籤/搜索