Kotlin中的Java 8 Stream API類比

1.簡介

Java 8將Streams的概念引入了集合層次結構。這些允許利用一些功能性編程概念使過程正常運行,從而以非常易讀的方式對數據進行非常強大的處理。

我們將研究如何通過使用Kotlin習語來實現相同的功能。我們還將介紹純Java中不可用的功能。

2. Java與Kotlin

在Java 8中,僅當與java.util.stream.Stream實例進行交互時,才可以使用新的高級API。

好消息是所有標準集合(任何實現java.util.Collection集合)都有一個可以生成Stream實例的特定方法stream()

重要的是要記住, Stream不是Collection.它沒有實現java.util.Collection ,也沒有實現Java中Collections任何常規語義。它更類似於一次性Iterator ,因為它是從Collection派生的,並用於遍歷Collection ,對可見的每個元素執行操作。

在Kotlin中,所有集合類型都已經支持這些操作,而無需先進行轉換。僅當集合語義錯誤時才需要進行轉換–例如,一個Set具有唯一的元素但無序。

這樣做的一個好處是,不需要使用Collection collect()調用將Collection初始轉換為Stream,也不需要將Stream最終轉換為Collection。

例如,在Java 8中,我們必須編寫以下代碼:

someList

 .stream()

 .map() // some operations

 .collect(Collectors.toList());

Kotlin中的等效項非常簡單:

someList

 .map() // some operations

此外,Java 8 Streams也不可重用。 Stream被消耗後,將無法再次使用。

例如,以下將不起作用:

Stream<Integer> someIntegers = integers.stream();

 someIntegers.forEach(...);

 someIntegers.forEach(...); // an exception

在Kotlin中,這些都是正常的收藏品這一事實意味著永遠不會出現此問題。中間狀態可以分配給變量并快速共享,並且可以按我們期望的那樣工作。

3.延遲序列

關於Java 8 Streams的關鍵之一是對它們進行延遲評估。這意味著將不會執行超出需要的工作。

如果我們要對Stream,的元素執行潛在的昂貴操作Stream,或者使用無限序列進行操作,則這特別有用。

例如, IntStream.generate將產生可能無限的整數Stream 。如果在其上調用findFirst() ,我們將獲取第一個元素,而不會遇到無限循環。

在科特林,收藏渴望而不是懶惰。這裡的例外是Sequence ,它的計算延遲。

如下面的示例所示,這是要注意的重要區別:

val result = listOf(1, 2, 3, 4, 5)

 .map { n -> n * n }

 .filter { n -> n < 10 }

 .first()

Kotlin版本將執行五個map()操作,五個filter()操作,然後提取第一個值。 Java 8版本將僅執行一個map()和一個filter()因為從最後一個操作的角度來看,不需要更多。

可以使用asSequence()方法將Kotlin中的所有集合轉換為惰性序列

在上面的示例中,使用Sequence代替List可以執行與Java 8中相同數量的操作。

4. Java 8 Stream操作

在Java 8中, Stream操作分為兩類:

  • 中間和
  • 終奌站

中間業務基本上是將一個Stream成另一種懶洋洋地-例如,一個Stream的所有整數的成Stream的所有偶數的。

終端選項是Stream方法鏈的最後一步,並觸發實際處理。

在科特林,沒有這種區別。相反,這些都是將集合作為輸入並產生新輸出的函數。

請注意,如果我們在Kotlin中使用eager集合,則會立即評估這些操作,與Java相比可能令人驚訝。如果我們需要讓它變得懶惰,請記住先轉換為Sequence

4.1。中級業務

Java 8 Streams API的幾乎所有中間操作在Kotlin中都具有等效功能。但是,這些不是中間操作(除了Sequence類的情況除外),因為它們會通過處理輸入集合而導致完全填充的集合。

在這些操作中,有幾項工作原理完全相同flatMap() filter()map()flatMap()distinct()sorted() –有些僅在名稱不同的情況下才工作相同limit()為現在take ,並且skip()現在是drop() 。例如:

val oddSquared = listOf(1, 2, 3, 4, 5)

 .filter { n -> n % 2 == 1 } // 1, 3, 5

 .map { n -> n * n } // 1, 9, 25

 .drop(1) // 9, 25

 .take(1) // 9

這將返回單個值“ 9” –3²。

其中一些操作還具有附加的版本(後綴“To” ),該版本輸出到提供的集合中,而不是生成新的集合。

這對於將多個輸入集合處理為相同的輸出集合很有用,例如:

val target = mutableList<Int>()

 listOf(1, 2, 3, 4, 5)

 .filterTo(target) { n -> n % 2 == 0 }

這會將值“ 2”和“ 4”插入到“目標”列表中。

通常唯一不能直接替換的操作是peek() –在Java 8中用於在處理管線中間迭代Stream中的條目而不會中斷該流。

如果我們使用的是惰性Sequence而不是一個急切的集合,那麼有一個onEach()函數可以直接替換peek函數。但是,這僅存在於該類中,因此我們需要知道我們使用哪種類型使其起作用。

標準中間操作還具有一些其他變體,可以使生活更輕鬆。例如, filter操作具有其他版本filterNotNull()filterIsInstance()filterNot()filterIndexed()

例如:

listOf(1, 2, 3, 4, 5)

 .map { n -> n * (n + 1) / 2 }

 .mapIndexed { (i, n) -> "Triangular number $i: $n" }

這將產生前五個三角數,形式為“三角數3:6”

另一個重要的區別是flatMap操作的工作方式。在Java 8中,需要執行此操作才能返回Stream實例,而在Kotlin中,可以返回任何集合類型。這使得使用起來更容易。

例如:

val letters = listOf("This", "Is", "An", "Example")

 .flatMap { w -> w.toCharArray() } // Produces a List<Char>

 .filter { c -> Character.isUpperCase(c) }

在Java 8中,第二行需要包裝在Arrays.toStream()中才能起作用。

4.2。終端機操作

Java 8 Streams API的所有標準終端操作都可以在Kotlin中直接替換,只有collect例外。

其中幾個確實有不同的名稱:

  • anyMatch() -> any()
  • allMatch() -> all()
  • noneMatch() -> none()

其中一些具有與Kotlin的區別方式有關的其他變體–有first()firstOrNull() ,如果集合為空,則first拋出,否則返回非空類型。

有趣的情況是collect 。 Java 8使用它可以使用提供的策略將所有Stream元素收集到某個集合中。

這允許提供一個任意的Collector ,該Collector將隨集合中的每個元素一起提供,並將產生某種輸出。這些是從Collectors幫助Collectors類中使用的,但是如果需要,我們可以編寫自己的類。

在Kotlin中,幾乎所有標準收集器都可以直接替換,這些收集器可以直接作為收集對象本身的成員使用-不需要提供收集器的額外步驟。

這裡的一個例外是summarizingDouble / summarizingInt / summarizingLong方法-一次生成平均值,計數,最小值,最大值和總和。它們中的每一個都可以單獨生產-儘管顯然成本更高。

另外,我們可以使用for-each循環來管理它,並在需要時手動處理它-不太可能同時需要所有這5個值,因此我們只需要實現那些重要的值即可。

5. Kotlin的其他操作

Kotlin向集合中添加了一些額外的操作,而這些操作如果沒有自己實現就無法在Java 8中實現。

如上所述,其中一些只是對標準操作的擴展。例如,可以進行所有操作,以便將結果添加到現有集合中,而不是返回新集合。

在許多情況下,對於有序元素集合,lambda不僅提供有問題的元素,而且還提供元素的索引是可能的,因此索引很有意義。

例如,還有一些操作充分利用了Kotlin的無效安全性。我們可以對List<String?>執行filterNotNull()以返回List<String> ,其中所有的null都將被刪除。

可以在Kotlin中完成但在Java 8 Streams中不能完成的實際附加操作包括:

  • zip()unzip() –用於將兩個集合合併為一個序列對,反之則將一對集合轉換為兩個集合
  • associate -用於通過提供一個lambda將集合中的每個條目轉換為結果映射中的鍵/值對來將集合轉換為映射

例如:

val numbers = listOf(1, 2, 3)

 val words = listOf("one", "two", "three")

 numbers.zip(words)

這將產生一個List<Pair<Int, String>> ,其值1 to “one”, 2 to “two”以及3 to “three”

val squares = listOf(1, 2, 3, 4,5)

 .associate { n -> n to n * n }

這將生成Map<Int, Int> ,其中鍵是數字1到5,值是這些值的平方。

6.總結

我們習慣於Java 8的大多數流操作都可以直接在Kotlin中的標準Collection類上使用,而無需先轉換為Stream

此外,Kotlin通過添加更多可以使用的操作以及現有操作的更多變體,為工作方式增加了更多靈活性。

但是,默認情況下,Kotlin渴望而不是懶惰。如果我們對正在使用的集合類型不小心,這可能導致需要執行其他工作。