Kotlin知識歸納(七) —— 集合

大棋發表於2019-06-26

前序

      Kotlin沒有自己的集合庫,完全依賴Java標準庫中的集合類,並通過擴充套件函式增加特性來增強集合。意味著Kotlin與Java互動時,永遠不需要包裝或者轉換這些集合物件,大大增強與Java的互操作性。

只讀集合和可變集合

      Kotlin與Java最大的不同之一就是:Kotlin將集合分為只讀集合和可變集合。這種區別源自最基礎的集合介面:kotlin.collections.Collection。該介面可以對集合進行一些基本操作,但無任何新增和移除元素的方法。

      只有實現 kotlin.collections.MutableCollection 介面才可以修改集合的資料。MutableCollection 介面繼承自 Collection,並提供新增、移除和清空集合元素的方法。當一個函式接收 Collection,而不是 MutableCollection,即意味著函式不對集合做修改操作。

Kotlin知識歸納(七) —— 集合
      可變集合一般都帶有 “Mutable” 字首修飾,意味著能對集合中的元素進行修改。 Iterable<T> 定義了迭代元素的操作, Collection 繼承自 Iterable<T> 介面,從而具有對集合迭代的能力。

Kotlin知識歸納(七) —— 集合

建立集合

      Kotlin中建立集合一般都是通過 Collection.kt 中的頂層函式進行建立。具體方法如下:

集合型別 只讀 可變
List listOf mutableList、arrayListOf
Set setOf mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf
Map mapOf mutableMapOf、hashMapOf、linkeMapOf、sortedMapOf

      像 arrayListOf 這些指明集合型別的頂層函式,建立時都是對應著Java相應型別的集合。為了弄清楚 Kotlin 的生成的只讀集合(listOfsetOfmapOf)與可變集合(mutableListmutableSetOfmutableMapOf)生成的是什麼Java型別集合,做了一個小實驗(分別對應空集合、單元素集合和多元素集合):

  • 1、使用在Java類中編寫一些列印集合型別的靜態方法:
#daqiJava.java
public static void collectionsType(Collection collection){
    System.out.println(collection.getClass().getName());
}

public static void mapType(Map map){
    System.out.println(map.getClass().getName());
}
複製程式碼
  • 2、在Kotlin中建立只讀集合和可變集合,並將其傳入之前宣告的Java靜態方法中進行列印:
#daqiKotlin.kt
fun main(args: Array<String>) {
    val emptyList = listOf<Int>()
    val emptySet = setOf<Int>()
    val emptyMap = mapOf<Int,Int>()
    
    val initList = listOf(1)
    val initSet = setOf(2)
    val initMap = mapOf(1 to 1)
    
    val list = listOf(1,2)
    val set = setOf(1,2)
    val map = mapOf(1 to 1,2 to 2)

    println("空元素只讀集合")
    collectionsType(emptyList)
    collectionsType(emptySet)
    mapType(emptyMap)
    
    println("單元素只讀集合")
    collectionsType(initList)
    collectionsType(initSet)
    mapType(initMap)
    
    println("多元素只讀集合")
    collectionsType(list)
    collectionsType(set)
    mapType(map)

    println("-----------------------------------------------------------------")

    val emptyMutableList = mutableListOf<Int>()
    val emptyMutableSet = mutableSetOf<Int>()
    val emptyMutableMap = mutableMapOf<Int,Int>()

    val initMutableList = mutableListOf(1)
    val initMutableSet = mutableSetOf(2)
    val initMutableMap = mutableMapOf(1 to 1)


    val mutableList = mutableListOf(1,2)
    val mutableSet = mutableSetOf(1,2)
    val mutableMap = mutableMapOf(1 to 1,2 to 2)

    println("空元素可變集合")
    collectionsType(emptyMutableList)
    collectionsType(emptyMutableSet)
    mapType(emptyMutableMap)

    println("單元素可變集合")
    collectionsType(initMutableList)
    collectionsType(initMutableSet)
    mapType(initMutableMap)

    println("多元素可變集合")
    collectionsType(mutableList)
    collectionsType(mutableSet)
    mapType(mutableMap)
}
複製程式碼

結果:

Kotlin知識歸納(七) —— 集合
      可以得出只讀集合(listOfsetOfmapOf)與可變集合(mutableListmutableSetOfmutableMapOf)對應Java集合的關係表:

方法 Java型別
listOf() kotlin.collections.EmptyList
setOf() kotlin.collections.EmptySet
mapOf() kotlin.collections.EmptyMap
listOf(element: T) java.util.Collections$SingletonList
setOf(element: T) java.util.Collections$SingletonSet
mapOf(pair: Pair<K, V>) java.util.Collections$SingletonMap
listOf(vararg elements: T) java.util.Arrays$ArrayList
setOf(vararg elements: T) java.util.LinkedHashSet
mapOf(vararg pairs: Pair<K, V>) java.util.LinkedHashMap
mutableList() java.util.ArrayList
mutableSetOf() java.util.LinkedHashSet
mutableMapOf() java.util.LinkedHashMap

型變

      只讀集合型別是型變的。當類 Rectangle 繼承自 Shape,則可以在需要 List<Shape> 的任何地方使用 List<Rectangle>。 因為集合型別與元素型別具有相同的子型別關係。 Map在值型別上是型變的,但在鍵型別上不是。

      可變集合不是型變的。 MutableList <Rectangle>MutableList <Shape> 的子型別,當你插入其他 Shape 的繼承者(例如,Circle),從而違反了它的 Rectangle 型別引數。

集合的可空性

      對於任何型別,都可以對其宣告為可空型別,集合也不例外。你可以將集合元素的型別設定為可空,也可以將集合本身設定為可空,需要清楚是集合的元素可空還是集合本身可空。

Kotlin知識歸納(七) —— 集合

Kotlin集合的祕密:平臺相關宣告

尋找java.util.ArrayList

      學習 Kotlin 的時候,常常被告知 Kotlin 直接使用的是原生 Java 集合,抱著探究真相的心態,點進了建立集合的頂層方法 mutableListOf()

#Collections.kt
public fun <T> mutableListOf(vararg elements: T): MutableList<T> =
    if (elements.size == 0) 
        ArrayList() 
    else
        ArrayList(ArrayAsCollection(elements, isVarargs = true))
複製程式碼

      在原始碼中看到了熟悉的ArrayList,那是Java的ArrayList嘛?繼續點進ArrayList,發現是一個Kotlin定義的ArrayList

#ArrayList.kt
expect class ArrayList<E> : MutableList<E>, RandomAccess {
    constructor()
    constructor(initialCapacity: Int)
    constructor(elements: Collection<E>)
    
    //... 省略一些來自List、MutableCollection和MutableList的方法
    //這些方法只有宣告,沒有具體實現。
}
複製程式碼

      逛了一大圈,並沒有找到一絲 Java 的 ArrayList 的痕跡.... Excuse me??? 說好的使用 Java 的 ArrayList ,但自己又建立了一個ArrayList.... 。最後將目標鎖定在類宣告的 expect 關鍵字,這是什麼?最後在Kotlin官網中查到,這是Kotlin 平臺相關宣告預期宣告

平臺相關宣告

      在其他語言中,通常在公共程式碼中構建一組介面,並在平臺相關模組中實現這些介面來實現多平臺。然而,當在其中某個平臺上已有一個實現所需功能的庫,並且希望直接使用該庫的API而無需額外包裝器時,這種方法並不理想。

      Kotlin 提供平臺相關宣告機制。 利用這種機制,公共模組中定義預期宣告,而平臺模組提供與預期宣告相對應的實際宣告

要點:

  • 公共模組中的預期宣告與其對應的實際宣告始終具有完全相同的完整限定名。
  • 預期宣告標有 expect 關鍵字;實際宣告標有 actual 關鍵字。
  • 與預期宣告的任何部分匹配的所有實際宣告都需要標記為 actual。
  • 預期宣告 決不包含任何實現程式碼。

官網提供一個簡單的例子:

#kt
//在公共模組中定義一個預期宣告(不帶任何實現)
expect class Foo(bar: String) {
    fun frob()
}

fun main() {
    Foo("Hello").frob()
}
複製程式碼

相應的 JVM 模組提供實現宣告和相應的實現:

#kt
//提供實際宣告
actual class Foo actual constructor(val bar: String) {
    actual fun frob() {
        println("Frobbing the $bar")
    }
}
複製程式碼

      如果有一個希望用在公共程式碼中的平臺相關的庫,同時為其他平臺提供自己的實現。(像Java已提供好完整的集合庫)那麼可以將現有類的別名作為實際宣告:

expect class AtomicRef<V>(value: V) {
  fun get(): V
  fun set(value: V)
  fun getAndSet(value: V): V
  fun compareAndSet(expect: V, update: V): Boolean
}

actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V>
複製程式碼

      而Java集合類作為實際宣告的別名被定義在 TypeAliases.kt 中。這是我不知道 TypeAliases.kt 時的查詢流程:

Kotlin知識歸納(七) —— 集合

# TypeAliases.kt
@SinceKotlin("1.1") public actual typealias RandomAccess = java.util.RandomAccess

@SinceKotlin("1.1") public actual typealias ArrayList<E> = java.util.ArrayList<E>
@SinceKotlin("1.1") public actual typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public actual typealias HashMap<K, V> = java.util.HashMap<K, V>
@SinceKotlin("1.1") public actual typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public actual typealias HashSet<E> = java.util.HashSet<E>
複製程式碼

      Kotlin定義一些集合類作為集合的通用層(使用 expect 定義預期宣告),並將現有的Java集合類的別名作為實際宣告,從而實現在JVM上直接使用Java的集合類。

ArrayList的變遷

可以從Kotlin官方文件中集合的變遷來觀察(ArrayList為例):

  • 1.0版本ArrayList:
    Kotlin知識歸納(七) —— 集合
  • 1.1版本ArrayList:
    Kotlin知識歸納(七) —— 集合
  • 1.3版本ArrayList:
    Kotlin知識歸納(七) —— 集合

      從原本無ArrayList.kt,只有一系列對ArrayList.java的擴充套件屬性與方法

-> 使用別名引用Java的ArrayList.java,ArrayList.kt服務於Js模組。

-> 使用平臺相關宣告,將ArrayList.kt作為預期宣告,並在JVM模組、Js模組、Native模組中提供具體的實際宣告。使Kotlin對外提供"通用層"API,在不改變程式碼的情況下,實現跨平臺。

只讀集合與平臺相關宣告

      當對應單個或多個初始化值的集合時,其使用的都是Java的集合型別,一起探究下是否也與平臺相關宣告有關:

單元素只讀集合

      建立單元素集合的listOf(element: T)setOf(element: T)mapOf(pair: Pair<K, V>)直接作為頂層函式宣告在JVM模組中,並直接使用Java的單元素集合類進行初始化。

#CollectionsJVM.kt
//listOf
public fun <T> listOf(element: T): List<T> =
java.util.Collections.singletonList(element)
複製程式碼
#SetsJVM.kt
//setOf
public fun <T> setOf(element: T): Set<T> =
java.util.Collections.singleton(element)
複製程式碼
#MapsJVM.kt
//mapOf
public fun <K, V> mapOf(pair: Pair<K, V>): Map<K, V> =
java.util.Collections.singletonMap(pair.first, pair.second)
複製程式碼

多元素只讀集合

      建立多元素集合的頂層函式的引數都帶有vararg宣告,這類似於Java的可變引數,接收任意個數的引數值,並打包為陣列。

  • listOf(vararg elements: T):
#Collections.kt
public fun <T> listOf(vararg elements: T): List<T> = 
if (elements.size > 0) elements.asList() else emptyList()
複製程式碼

listOf(vararg elements: T)函式會直接將可變引數轉換為list:

#_Arrays.kt
public expect fun <T> Array<out T>.asList(): List<T>
複製程式碼

Array.asList()擁有 expect 關鍵字,即作為預期宣告存在,這意味著JVM模組會提供對應的實現:

#_ArraysJvm.kt
public actual fun <T> Array<out T>.asList(): List<T> {
    return ArraysUtilJVM.asList(this)
}
複製程式碼
#ArraysUtilJVM.java
class ArraysUtilJVM {
    static <T> List<T> asList(T[] array) {
        return Arrays.asList(array);
    }
}
複製程式碼

在JVM模組中提供了實際宣告的Array.asList(),並呼叫了java.util.Arrays.asList(),返回java.util.Arrays的靜態內部類java.util.Arrays$ArrayList物件。

  • setOf(vararg elements: T):
#Sets.kt
public fun <T> setOf(vararg elements: T): Set<T> = 
if (elements.size > 0) elements.toSet() else emptySet()
複製程式碼

setOf(vararg elements: T)函式會直接將可變引數轉換為set:

public fun <T> Array<out T>.toSet(): Set<T> {
    return when (size) {
        0 -> emptySet()
        1 -> setOf(this[0])
        else -> toCollection(LinkedHashSet<T>(mapCapacity(size)))
    }
}
複製程式碼

並和mutableSetOf()一樣,使用Kotlin的LinkedHashSet依託平臺相關宣告建立java.util.LinkedHashSet物件。(具體轉換邏輯不深究)

  • mapOf(vararg pairs: Pair<K, V>)
public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> =
    if (pairs.size > 0) pairs.toMap(LinkedHashMap(mapCapacity(pairs.size))) else emptyMap()
複製程式碼

並和mutableMapOf()一樣,使用Kotlin的LinkedHashMap依託平臺相關宣告建立java.util.LinkedHashMap物件。(具體轉換邏輯不深究)

集合的函式式API

      瞭解了一波Kotlin的集合後,需要回歸到對集合的使用上——集合的函式式API。

filter函式

基本定義:

      filter函式遍歷集合並返回給定lambda中返回true的元素。

原始碼:

#_Collection.kt
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    //建立一個新的集合並連同lambda一起傳遞給filterTo()
    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) 
        //執行lambda,如返回為true,則將該元素新增到新集合中
        if (predicate(element)) 
            destination.add(element)
    //返回新集合
    return destination
}
複製程式碼

解析:

      建立一個新的ArrayList物件,遍歷原集合,將lambda表示式返回true的元素新增到新ArrayList物件中,最後返回新的ArrayList物件。

Kotlin知識歸納(七) —— 集合

map函式

基本定義:

      map函式對集合中每一個元素應用給定的函式,並把結果收集到一個新集合。

原始碼:

#_Collection.kt
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    //建立一個新的集合並連同lambda一起傳遞給mapTo()
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    //遍歷舊集合元素
    for (item in this)
        //執行lambda,對元素進行處理,將返回值新增到新集合中
        destination.add(transform(item))
    //返回新集合
    return destination
}
複製程式碼

解析:

      建立一個新的ArrayList集合,遍歷原集合,將函式型別物件處理過的值新增到新ArrayList物件中,並返回新的ArrayList物件。

Kotlin知識歸納(七) —— 集合

groupBy函式

基本定義:

      對集合元素進行分組,並返回一個Map集合,儲存元素分組依據的鍵和元素分組

原始碼:

#_Collection.kt
public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
    //建立一個新的map並連同lambda一起傳遞給groupByTo()
    return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}

public inline fun <T, K, M : MutableMap<in K, MutableList<T>>> Iterable<T>.groupByTo(destination: M, keySelector: (T) -> K): M {
    //遍歷舊集合元素
    for (element in this) {
        //執行lambda,對元素進行處理,將返回值作為key
        val key = keySelector(element)
        //使用得到的key在新的map中獲取vlaue,如果沒有則建立一個ArrayList物件,作為value儲存到map中,並返回ArrayList物件。
        val list = destination.getOrPut(key) { ArrayList<T>() }
        //對ArrayList物件新增當前元素
        list.add(element)
    }
    //返回新集合
    return destination
}
複製程式碼

解析:

      建立一個LinkedHashMap物件,遍歷舊集合的元素,將函式型別物件處理過的值作為key,對應的元素儲存到一個ArrayList中,並將該ArrayList物件作為mapvalue進行儲存。返回LinkedHashMap物件。

Kotlin知識歸納(七) —— 集合

flatMap函式

基本定義:

      根據實參給定的函式對集合中的每個元素做交換(對映),然後把多個列表平鋪成一個列表。

原始碼:

#_Collection.kt
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
    //建立一個新的集合並連同lambda一起傳遞給flatMapTo()
    return flatMapTo(ArrayList<R>(), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {   
    ////遍歷舊集合元素
    for (element in this) {
        //執行lambda,對元素進行處理,返回一個集合
        val list = transform(element)
        //在得到的集合新增到新的集合中。
        destination.addAll(list)
    }
    //返回新集合
    return destination
}
複製程式碼

解析:

      建立一個新的ArrayList集合,遍歷原集合,對原集合的元素轉換成列表,最後將轉換得到的列表儲存到新的ArrayList集合中,並返回新的ArrayList物件。

Kotlin知識歸納(七) —— 集合

all函式 和 any函式

基本定義:

      檢查集合中的所有元素是否都符合或是否存在符合的元素。

原始碼:

#_Collection.kt
//any
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
    //判斷他是否為空,如果集合為空集合,直接返回false,因為肯定不存在
    if (this is Collection && isEmpty()) 
        return false
    for (element in this) 
        //遍歷元素的過程中,如果有其中一個元素滿足條件,則直接返回true
        if (predicate(element)) 
            return true
    //最後都不行,就返回false
    return false
}

//all
public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean {
    //如果集合為空集合,直接返回true
    if (this is Collection && isEmpty()) 
        return true
    for (element in this) 
        //遍歷元素的過程中,只要有其中一個元素不滿足條件,則直接返回false
        if (!predicate(element)) 
            return false
    return true
}
複製程式碼

count函式

基本定義:

      檢查有多少滿足條件的元素數量。

原始碼:

#_Collection.kt
public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int {
    if (this is Collection && isEmpty()) 
        return 0
    //弄一個臨時變數記錄數量
    var count = 0
    //遍歷元素
    for (element in this) 
        //如果滿足新增,則數量+1
        if (predicate(element)) 
            checkCountOverflow(++count)
    return count
}
複製程式碼

find函式

基本定義:

      尋找第一個符合條件的元素,如果沒有符合條件的元素,則返回null

原始碼:

#_Collection.kt
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
    //將lambda傳給firstOrNull()
    return firstOrNull(predicate)
}

public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? {
    for (element in this) 
        //遍歷的元素中,返回第一個符合滿足新增的元素。
        if (predicate(element)) 
            return element
    //沒找到,則返回null
    return null
}
複製程式碼

集合使用的注意事項

  • 優先使用只讀集合,只有在需要修改集合的情況下才使用可變集合。
  • 只讀集合不一定是不可變的。如果你使用的變數是隻讀介面的型別,該變數可能引用的是一個可變集合。因為只讀介面Collection是所有集合的"基類"
  • 只讀集合並不總是執行緒安全的。如果需要在多執行緒環境中處理資料,必須使用支援併發訪問的資料結構。

陣列

      Kotlin陣列是一個帶有型別引數的類,其元素型別被指定為相應的型別引數。

在Kotlin中提供以下方法建立陣列:

  • arrayOf函式,該函式的實參作為陣列的元素。
  • arrayOfNulls函式,建立一個給定大小的陣列,包含的是null值。一般用來建立元素型別可空的陣列
  • Array構造方法,接收一個陣列的大小和lambda表示式。lambda表示式用來建立每一個陣列元素,不能顯式地傳遞每一個元素。
val array = Array<String>(5){
    it.toChar() + "a"
}
複製程式碼

      Kotlin最常見的建立陣列的情況是:呼叫需要陣列為引數的Java方法,或呼叫帶有vararg引數的Kotlin函式。這時需要使用toTypeArray()將集合轉換成陣列。

val list = listOf("daqi","java","kotlin")
//集合轉陣列
list.toTypedArray()

val array = arrayOf("")
//陣列轉集合
array.toList()
複製程式碼

      Array類的型別引數決定了建立的是一個基本資料型別裝箱的陣列。當需要建立沒有裝箱的基本資料型別的陣列時,必須使用基本資料型別陣列。Kotlin為每一種基本資料型別提供獨立的基本資料型別陣列。例如:Int型別的陣列叫做IntArray。基本資料型別陣列會被編譯成普通的Java基本資料型別的陣列,如int[].因此基本資料型別陣列在儲存值時並沒有裝箱。

建立基本資料型別陣列:

  • 工廠方法(例如intArrayOf)接收變長引數並建立儲存這些值的陣列。
  • 基本資料型別陣列的構造方法。

      Kotlin標準庫中對集合的支援擴充套件庫(filtermap等)一樣適用於陣列,包括基本資料型別的陣列。

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

Kotlin知識歸納(七) —— 集合

相關文章