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渴望而不是懶惰。如果我們對正在使用的集合類型不小心,這可能導致需要執行其他工作。