Dive Into Kotlin(三):集合

本文由 Prefert 發表在 ScalaCool 團隊博客。html

在 Java/Android 開發中,咱們常常用集合來處理數據。java

Java 中的集合相對而言是比較簡單的,可是在不少時候,語法顯得冗長。git

Java 傳統集合 vs Java 8 Stream vs Kotlin 集合

咱們以文章(Article)爲例子,一篇文章有一個標題、做者及多個標籤:github

public class Article {

    private String title;
    private String author;
    private List<String> tags;

    // ... some get、set、construct method
}
複製代碼

如今有一個需求:將全部文章(Article)按做者(author)進行分組。編程

Java 實現以下:閉包

private static Map<String, List<Article>> groupByAuthor(List<Article> articles) {
    Map<String, List<Article>> result = new HashMap<>();
    for (Article article : articles) {
        if (result.containsKey(article.getAuthor())) {
            result.get(article.getAuthor()).add(article);
        } else {
            ArrayList<Article> articlesTemp = new ArrayList<>();
            articlesTemp.add(article);
            result.put(article.getAuthor(), articlesTemp);
        }
    }
    return result;
}
複製代碼

Kotlin 因爲高度兼容 Java 而愈來愈受歡迎,最重要的仍是它簡潔的語法(本篇僅論集合層面),上面的代碼在 Kotlin 中能夠寫爲:oracle

private fun groupByAuthorKotlin(articles: List<Article>): Map<String, List<Article>> {
    return articles.groupBy { it.author }
}
複製代碼

鏈式調用是否是很優雅?框架

使用 Java 8 的同窗可能會表示不服(鏈式調用我也行!):ide

private static Map<String, List<Article>> groupByAuthorStream(List<Article> articles) {
    return articles.stream()
            .collect(Collectors.groupingBy(Article::getAuthor));
}
複製代碼

除了代碼量上的優點,語法上也更能體現業務需求,便於維護。這也是愈來愈多的開發者喜歡函數式的緣由之一。(關於 Stream 與 Kotlin 的對決將呈如今文章後半部分)函數式編程

以上,相信你已經對 Kotlin 集合產生興趣了,接下去讓咱們一塊兒來看看 Kotlin 集合的結構。

一. Kotlin 集合的結構

咱們都知道 Kotlin 集合基於 (www.tutorialspoint.com/java/java_c…)[Java 集合框架]。

理所應當,它的核心也是 Iterator

Iterator

做爲一個 Java 開發者,咱們都知道 Iterator 主要的做用就是提供遍歷的能力。

可是,Kotlin 將集合分紅了兩類: 「可變集合」 與 「不可變集合」。形成Iterator 層級核心變更以下:

  • ListIterator 僅支持遍歷。
  • MutableIterator 提供刪除元素的能力。
  • MutableListIterator 繼承以上兩個接口,具有了新增元素的能力

即:

iterator

Hint: Kotlin 中 out 關鍵字表明這個類的對象爲只讀。

List && Set

由以上,咱們也能夠推測出,List 以及Set的結構變更,最關鍵且惟一的變化就是區分了可變集合。

總體結構能夠參考下圖:

kotlin collection hierarchy

與 Java 相比,Kotlin 集合的層次結構更加詳細——這也是 Java 摸爬滾打產生的更好的實踐。

二. Kotlin 的集合操做符

若是你使用過 RxJava 等一系列庫,你必定會對操做符很是瞭解也對操做符的強大深有感觸。

Kotlin 也如此,原生便支持大量操做符,先上一部分感覺一下:

分類 方法
元素操做 contains / elementAt / firstOrNull / lastOrNull / indexOf / singleOrNull
判斷操做 any / all / none / count / reduce / fold
過濾操做 filter / filterNot / filterNotNull / take / min / max
集合轉換 map / mapIndexed / mapNotNull / flatMap / groupBy / zip
排序 reversed / sorted / sortedBy / sortedDescending

Hint:能夠在 _Collections.kt 中看到全部的操做符。

Talk is cheap ! 咱們舉幾個例子:

過濾 filter 與變換 map

// 定義並初始化列表
val list = listOf(1, 2, 3, 4, 5, 6)

println(list.filter { it % 2 == 0 })
// [2, 4, 6]

println(list.map { it * it })
// [1, 4, 9, 16, 25, 36]
複製代碼

觀察結果可知:

filter 函數遍歷集合並返回了符合條件元素的集合。

kotlin-filter

map 函數遍歷集合並對每一個元素作出了相同的處理。

kotlin-map

平鋪 flatten 與變換平鋪 flatMap

val words = listOf(listOf("kotlin"), listOf("is", "best"))
println(words.flatten())
// [kotlin, is, best]
println(words.flatMap { it.map(String::toUpperCase) })
// [KOTLIN, IS, BEST]
複製代碼

觀察結果可知:

flatten 函數能夠將多個列表形式的元素平鋪,就好像給每一個元素脫掉了衣服,再將他們包在一塊兒。

flatMap 函數但是說是 flatten 的增強版,能夠先將子列表進行變換後再平鋪,再將他們包在一塊兒。

kotlin-flatMap

操做符的實現

對於沒有接觸過函數式編程的朋友,可能會不由發問: Kotlin 爲何可以實現這樣的騷操做?

這些方法咱們從最簡單的 filter 入手。

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
複製代碼

以上,不難看出 Kotlin 中集合操做符本質上就是方法調用。

filter 實際上是 Itrable 的一個擴展方法 (extention),它接收一個 T 做爲參數,並返回 Boolean 的閉包做爲參數,內部調用了 filterTo 方法。

再看看 filterTo 方法:傳入了目標類型 C 和判斷用閉包。內部實際就是循環對元素判斷,符合則添加到返回的集合中。

是否是很簡單?

咱們嘗試實現相似 mapfilter 結合的方法 magicConvert

private fun <T, E> Iterable<T>.collect(function: (T) -> E, predicate: (T) -> Boolean): MutableList<E> {
    val result: MutableList<E> = mutableListOf()
    for (element in this) if(predicate(element)) result.add(function(element))
    return result
}

// Test
println(list.collect({ it * it }, { it % 2 == 0 }))
// [4, 16, 36]
複製代碼

至此,咱們應該已經對 Kotlin 集合的操做有了基本瞭解。

三. 對比 Kotlin Collections 和 Java 8 Stream

對於使用過 RxJava 的你,必定對 Java Stream有所瞭解。

文章開頭的例子已經展現過,在 Java 8 中, stream() 方法使得 Java 傳統的 Collection 類擁有了函數式的操做。

這種語法相較 Kotlin 來講稍微顯得繁瑣了一點,每次操做前都須要轉換成 stream ,操做完還要 調用 collect() 轉換回 Collection。

例如:

// Java
someList
  .stream()
  .map() // some operations
  .collect(Collectors.toList());
複製代碼
// Kotlin
someList
  .map() // some operations
複製代碼

可是這麼作,實際上是有緣由:stream 只能被消費一次,不可屢次重用

下面這樣的操做會拋出異常:

Stream<Integer> someIntegers = integers.stream();
someIntegers.forEach(...);
someIntegers.forEach(...); // an exception
複製代碼

Kotlin 中由於 操做的中間狀態被快速地分配給了變量 ,運行起來並無任何問題。

延遲序列

Java 8 Stream 一個關鍵的點是:它使用了惰性求值(Lazy Evaluation),即在須要的時候纔會求值

Kotlin 則相反(除了 sequences,將在 Lambda 章節講述),採用及早求值(Eager Evaluation)。

舉個例子:

val result = listOf(1, 2, 3, 4, 5)
  .map { n -> n * n }
  .filter { n -> n < 10 }
  .first()
複製代碼

以上代碼,在 Kotlin 的版本中將執行 5 次 map()filter() 操做,最後返回第一個值。而在 Java Stream 中集合操做只會各執行 1 次。

在對性能有要求的場景下,咱們須要 使用 asSequence() 方法將集合轉爲惰性序列,以最小開銷來實現業務。

操做符

Java Stream 的中間操做與 Kotlin 幾乎沒有差異。

須要注意的幾個點是:

  • Java Stream 有一個peek() 方法用於不間斷的迭代 Stream 流。
  • Java Stream 的 flatMap() 方法須要返回 Stream 實例(須要用 Arrays.toStream()處理),而 Kotlin 能夠返回任何類型
  • Java Stream 的部分 lambda 表達式不包含索引,僅有元素。
  • 另外,Java Stream 目前並不支持zip ()unzip()associate() 操做。

四. 總結

本篇文章簡述了 Kotlin 集合的結構,揭露集合操做符的部分本質 並 初探擴展函數。

其次,經過與 Java 8 Stream 的比較,咱們能感覺到 Kotlin 以及函數式編程的優點與魅力。

固然,Kotlin 的黑魔法不止於此。

下一篇,咱們將討論 Kotlin 中的泛型和協變。


參考:

相關文章
相關標籤/搜索