Java 8 vs Scala?—?Part II Streams API

jopen 9年前發布 | 12K 次閱讀 Java 8

這是本文的第 2 部分。第 1 部分在這里

Stream 與 Collection 的比較

這是我按自己的意思給的一個十分簡要的說明:collection 是一個有限的數據集,而 stream 是數據的一個序列,可以是有限的也可以是無限的。區別就這么簡單。

Streams API 是 Java 8 的一個新的 API,用于操作 collection 和 stream 數據。Collections API 會改變數據集的狀態,而 Streams API 不會。例如,調用 Collections.sort(list) 會把傳入的參數排好序,而 list.stream().sorted() 則不同,它會把數據復制一份,保持原數據不變。Streams API 可以參考這里

下面是我從 Java 8 的文檔中摘出來的關于 collections 和 streams 的比較。強烈建議你看一下完整的版本

Streams 和 collections 幾個不同之處:

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

2. 本質是函數。在 steam 上的一個操作就產生一個新的結果,而不對數據源做任何的改動。

3. 懶執行的。許多 steam 的操作,如 filtering,mapping 或者 duplicate removal 都是懶執行的,使其能進行更好的優化。

4. 可能不受限制的。Collection 的大小是有限制的,streams 則沒有。

5. 消耗的。Steam 中的元素在 steam 的生存時間內只能被訪問一次。

Java 和 Scala 都有一個十分簡單的方式去同時計算 collection 中的值。在 Java 中,你只需要使用parallelStream()* 或者 stream().parallel(),而不是簡單的使用 stream()。在 Scala 中,在使用其他方法之前必須先調用 par()。而且可以通過添加并行度來提高程序的性能。不幸的是,大多數時間它的執行速度都是非常慢的。事實上,parallelism 是一個很容易被錯誤的使用組件。查看這個文章Java Parallel Streams Are Bad for Your Health!

* 在 JavaDoc 中,關于parallelStream() 方法是這樣說明的,這個方法可能會返回一個并行stream,那么意味著它也可能返回的是一個串行 stream。看到這里你一定會覺得很奇怪,有人已經就這個問題進行了研究,詳細情況請閱讀下面鏈接中的文章。 (someone did some research on why this API exists)

Java 8 vs Scala?—?Part II Streams API

Java 的 Stream API 是延后執行的。這就意味著,如果你在調用 stream API 的時候,沒有指定一個終結操作(比如 collect() 方法調用),那么所有的中間調用(比如 filter 調用)是不會被執行的。這樣做的主要目的是為了優化 stream API 的執行,并提高 stream API 的執行效率。比如,我們要對一個數據流進行過濾,映射,求和運算,通過使用延后執行機制,那么對所有這些操作只要遍歷一遍數據流就可以了。同時延后執行能力,也實現了每個操作只處理感興趣的的數據(數據經過前一個操作之后才傳入下一個操作)。 而對于 Scala,默認的集合是非延后處理的,這意味著每個操作都會完全遍歷 Collection 中的每個元素。這樣是否意味著,在我們的測試中,Java Stream API 應該優于 Scala 的呢?如果我們只是在 Java Stream API 和 Scala Collection API 之間做比較,那么答案是正確的,Java Stream API 要優于 Scala Collection API。但是在 Scala 中,你可以通過一個簡單的 toStream() 調用,將一個 Collection 轉換成一個 Stream。就算你不把 Collection 轉成Stream,在 Scala 中你還可以使用 view (一種提供延后處理能力的 Collection)來處理你的數據集合。

讓我們快速的看一下 Scala 的 Stream 和 View 特性。

Scala 的 Stream

Scala 的 Stream 和 Java 的 Stream 有點不同。在 Scala 的 Stream 中,你無須去調用終端操作去取的 Stream 的結果,因為它本身就是一個結果。Stream 是繼承 AbstractSeq, LinearSeq, 和 GenericTraversableTemplate 的一個抽象類。所以你可以把 Stream 看做一個Seq 。如果你不怎么熟悉 Scala,可以把 Seq 看做 Java 中的 List。(Scala 中的 List 不是一個接口,當然這個另作討論:)).

我們必須要知道 Streams 中的元素都是懶計算的,也正因如此 Stream 可以計算無限的數據。如果要計算幾個集合里的所有元素,Stream 和 List 有著相同的計算效率。一旦被使用,它的值就被 cache了。Stream 有一個叫 force 的方法,它強制評估整個 stream 并返回結果。在計算無限數據的時候千萬不要使用這個方法。還有 size(),toList(),foreach() 這些強制計算這個 Stream 的方法。這些操作在 Scala 的 Stream 中都是隱式的。

在 Scala 的 Stream 中實現斐波那契數列。

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 //不要這么使用,因為它會無限的執行下去,然后報內存溢出錯誤。
//fib1.size //不要這么使用和上面同樣的原因。
fib1.take(10) //將返回前10個值
fib1.take(20).foreach(println(_)) //打印前20個值

:: 是集合中常用的連接數據的方法。而 #:: 方法則是連接數據但是是懶執行的(Scala中的方法名是比較隨意的)。

Scala 的 View

再次重申,Scala 中的 collection 是一個樣的 collection 而 View 則是一個非嚴格的 collection。View 是基于一個基礎 collection 的 collection,其中所有的轉換都是懶執行的。通過調用 view 方法可以把一個嚴格的 collection 轉換成 view,也可以通過調用 force 方法把它轉換回來。View 并不 cache 結果,每次你調用它的時候它都會執行一次。就像數據庫的 View,但它是虛擬的集合。

創建一個要使用的數據集。

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

這種方式是我們在命令式語言中常見的。你必須創建一個臨時的集合,之后遍歷每個元素并存儲每一個滿足謂詞的元素放入這個臨時的集合。有點啰嗦,但其所做的工作和其性能一樣,是驚人的。在這里,我會破壞這種更快的傳統流式 API 方式。不要擔心性能,因為那會讓代碼更優雅,這超過了輕微的性能增益。

Java Approach 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 去過濾集合中的元素。我故意調用兩次 filter 是想展示Streams 的 API 設計的就像是一個 Builder pattern。在 Builder pattern 中,在構造結果集前你可以把一系列方法串聯起來使用。在 Streams API 中,構造方法被叫做裝卸操作。中間操作不是一個裝卸操作。裝卸操作可能和構造方法有些不同,因為它在 Streams API 中只能被調用一次。有很多你可以使用的裝卸操作 --collect,count,min,max,iterator,toArray。這些操作產生的結果和一些裝卸操作一樣會消耗其中的值,例如,foreach。你認為傳統和 Streams API 哪一個可讀性更強?

Java Approach 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’s law,那樣的話代碼看起來就像是第二個聲明。

Scala 入門:集合,視圖,與流

//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 中流 API。看看那每一個,你不得不調用視圖函數把嚴格的集合轉向非嚴格的集合,并且調用 tostream 函數,把嚴格的集合轉向一個流。

我認為,我已經有了這個想法,因此,我將向你顯示該代碼,并且保持沉默。

分組

元素屬性中的一個元素中的組元素。該結果將是地圖<T,列表<T>>,和一個泛型類型。

要求:通過其類型中組寵物,諸如狗,貓等等。

//Java approach
Map<Pet.Type, List<Pet>> result = pets.stream().collect(groupingBy(Pet::getType));
//Scala approach
val result = pets.groupBy(_.getType)

排序

集合中的任何屬性元素中的各種元素。結果將是任何類型的集合,依靠配置,來維持元素的秩序。

要求:我們要按類型、名稱和色序來給寵物分類。


//Java approach
pets.stream().sorted(comparing(Pet::getType)
    .thenComparing(Pet::getName)  
    .thenComparing(Pet::getColor))
    .collect(toList());
//Scala approach
pets.sortBy{ p => (p.getType, p.getName, p.getColor) }

Mapping

在集合中每個元素上應用給定的方法。根據你給定義的方法不同返回的結果類型也不同。

需求: 我們想把寵物類轉換成“%s — name: %s, color: %s”格式。

//Java 方法
pets.stream().map( p-> 
        String.format(“%s — name: %s, color: %s”, 
            p.getType(), p.getName(), p.getColor())
    ).collect(toList());
//Scala 方法
pets.map{ p => s"${p.getType} - name: ${p.getName}, color: ${p.getColor}"}

Finding First

返回第一個和給定值匹配的值.

需求:我們想找一個名叫 “Handsome”的寵物。 不管有多少個“Handsome",只取第一個。

//Java 方法

pets.stream()

    .filter( p-> p.getName().equals(“Handsome”))

    .findFirst();</pre>

//Scala 方法
pets.find{ p=> p.getName == “Handsome” }

這個有點狡猾。你有注意到在 Scala 中我使用的是 find 而不是 filter 方法嗎?如果用 filter 代替 find,它就會讀取所有的元素,因為 scala 的集合嚴格的。然而,在 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個寵物的名字。這就是集合懶執行的好處,連計算都是懶的。

pets.view.filter { x => println(x.getName); true }.get(1) --- (3)

(3)式和(2)式會有一樣的結果嗎?答案是不是,它的結果和(1)是一樣的,你知道為什么嗎?

通過比較 Java 和 Scala 中的一些共同的操作方法 --filter,group,map 和 find;很明顯 Scala 的方法比 Java 的簡潔。你更喜歡哪一個呢?哪一個是更可讀的?

在文章的下一個部分,我們將比較哪一個比較快。它是可以準確比較的。請保持關注。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!