Kotlin知识归纳(七) —— 集合
前序
Kotlin没有自己的集合库,完全依赖Java标准库中的集合类,并通过扩展函数增加特性来增强集合。意味着Kotlin与Java交互时,永远不需要包装或者转换这些集合对象,大大增强与Java的互操作性。
只读集合和可变集合
Kotlin与Java最大的不同之一就是:Kotlin将集合分为只读集合和可变集合。这种区别源自最基础的集合接口:kotlin.collections.Collection
。该接口可以对集合进行一些基本操作,但无任何添加和移除元素的方法。
只有实现 kotlin.collections.MutableCollection
接口才可以修改集合的数据。MutableCollection
接口继承自 Collection
,并提供添加、移除和清空集合元素的方法。当一个函数接收 Collection
,而不是 MutableCollection
,即意味着函数不对集合做修改操作。
可变集合一般都带有 “Mutable” 前缀修饰,意味着能对集合中的元素进行修改。
Iterable<T>
定义了迭代元素的操作, Collection 继承自 Iterable<T>
接口,从而具有对集合迭代的能力。
image
创建集合
Kotlin中创建集合一般都是通过 Collection.kt 中的顶层函数进行创建。具体方法如下:
集合类型 | 只读 | 可变 |
---|---|---|
List | listOf | mutableList、arrayListOf |
Set | setOf | mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf |
Map | mapOf | mutableMapOf、hashMapOf、linkeMapOf、sortedMapOf |
像 arrayListOf 这些指明集合类型的顶层函数,创建时都是对应着Java相应类型的集合。为了弄清楚 Kotlin 的生成的只读集合(listOf
、setOf
和 mapOf
)与可变集合(mutableList
、mutableSetOf
和 mutableMapOf
)生成的是什么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)
}
结果:
image可以得出只读集合(
listOf
、setOf
和 mapOf
)与可变集合(mutableList
、mutableSetOf
和 mutableMapOf
)对应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
类型参数。
集合的可空性
对于任何类型,都可以对其声明为可空类型,集合也不例外。你可以将集合元素的类型设置为可空,也可以将集合本身设置为可空,需要清楚是集合的元素可空还是集合本身可空。
imageKotlin集合的秘密:平台相关声明
寻找java.util.ArrayList<E>
学习 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 时的查找流程:
image# 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:
image -
1.1版本ArrayList:
image -
1.3版本ArrayList:
image
从原本无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
对象。
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
对象。
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
对象作为map
的value
进行存储。返回LinkedHashMap
对象。
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
对象。
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标准库中对集合的支持扩展库(filter
、map
等)一样适用于数组,包括基本数据类型的数组。
参考资料:
- 《Kotlin实战》
- Kotlin官网