Kotlin集合的创建与遍历
九路 423 4

创建集合

创建集合也叫构造一个集合

创建集合的最常用方法是使用标准库函数 listOf<T>()setOf<T>()mutableListOf<T>()mutableSetOf<T>()。 如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。

创建空集合时,须明确指定类型。

val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()

同样的,Map 也有这样的函数 mapOf()mutableMapOf()。映射的键和值作为 Pair 对象传递(通常使用中缀函数 to 创建)。

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

注意,to 符号创建了一个短时存活的 Pair 对象,因此建议仅在性能不重要时才使用它。 为避免过多的内存使用,请使用其他方法。例如,可以创建可写 Map 并使用写入操作填充它。 apply() 函数可以帮助保持初始化流畅。

val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }

空集合

还有用于创建没有任何元素的集合的函数:emptyList()emptySet()emptyMap()。 创建空集合时,应指定集合将包含的元素类型。

val empty = emptyList<String>()

list 的初始化函数

对于 List,有一个接受 List 的大小与初始化函数的构造函数,该初始化函数根据索引定义元素的值。

val doubled = List(3, { it * 2 })  // 如果你想操作这个集合,应使用 MutableList
println(doubled)

具体类型构造函数

要创建具体类型的集合,例如 ArrayListLinkedList,可以使用这些类型的构造函数。 类似的构造函数对于 SetMap 的各实现中均有提供。

val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val presizedSet = HashSet<Int>(32)

复制

要创建与现有集合具有相同元素的集合,可以使用复制操作。 标准库中的集合复制操作创建了具有相同元素引用的 浅 复制集合。 因此,对集合元素所做的更改会反映在其所有副本中。

在特定时刻通过集合复制函数,例如toList()toMutableList()toSet() 等等。创建了集合的快照。

结果是创建了一个具有相同元素的新集合 如果在源集合中添加或删除元素,则不会影响副本。副本也可以独立于源集合进行更改。

val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Copy size: ${copyList.size}")   

//readOnlyCopyList.add(4)             // 编译异常
println("Read-only copy size: ${readOnlyCopyList.size}")

这些函数还可用于将集合转换为其他类型,例如根据 List 构建 Set,反之亦然。

val sourceList = mutableListOf(1, 2, 3)    
val copySet = sourceList.toMutableSet()
copySet.add(3)
copySet.add(4)    
println(copySet)

或者,可以创建对同一集合实例的新引用。使用现有集合初始化集合变量时,将创建新引用。 因此,当通过引用更改集合实例时,更改将反映在其所有引用中。

val sourceList = mutableListOf(1, 2, 3)
val referenceList = sourceList
referenceList.add(4)
println("Source size: ${sourceList.size}")

集合的初始化可用于限制其可变性。例如,如果构建了一个 MutableListList 引用,当你试图通过此引用修改集合的时候,编译器会抛出错误。

val sourceList = mutableListOf(1, 2, 3)
val referenceList: List<Int> = sourceList
//referenceList.add(4)            // 编译错误
sourceList.add(4)
println(referenceList) // 显示 sourceList 当前状态

调用其他集合的函数

可以通过其他集合各种操作的结果来创建集合。例如,过滤列表会创建与过滤器匹配的新元素列表:

val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)

映射生成转换结果列表:

val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })

关联生成 Map:

val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })

迭代器

对于遍历集合元素, Kotlin 标准库支持 迭代器 的常用机制——对象可按顺序提供对元素的访问权限,而不会暴露集合的底层结构。 当需要逐个处理集合的所有元素(例如打印值或对其进行类似更新)时,迭代器非常有用。

Iterable<T> 接口的继承者(包括 SetList)可以通过调用 iterator() 函数获得迭代器。 一旦获得迭代器它就指向集合的第一个元素;

调用 next() 函数将返回此元素,并将迭代器指向下一个元素(如果下一个元素存在)。 一旦迭代器通过了最后一个元素,它就不能再用于检索元素;

也无法重新指向到以前的任何位置。要再次遍历集合,请创建一个新的迭代器。

代码如下:

val numbers = listOf("one", "two", "three", "four")
val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) {
    println(numbersIterator.next())
}

遍历 Iterable 集合的另一种方法是众所周知的 for 循环。 在集合中使用 for 循环时,将隐式获取迭代器。 因此,以下代码与上面的示例等效:

val numbers = listOf("one", "two", "three", "four")
for (item in numbers) {
    println(item)
}

最后,有一个好用的 forEach() 函数,可自动迭代集合并为每个元素执行给定的代码。因此,等效的示例如下所示:

val numbers = listOf("one", "two", "three", "four")
numbers.forEach {
    println(it)
}

List 迭代器

对于列表,有一个特殊的迭代器实现: ListIterator 它支持列表双向迭代: 正向与反向。 反向迭代由 hasPrevious()previous() 函数实现。 此外, ListIterator 通过 nextIndex()previousIndex() 函数提供有关元素索引的信息。

val numbers = listOf("one", "two", "three", "four")
val listIterator = numbers.listIterator()
while (listIterator.hasNext()) listIterator.next()
println("Iterating backwards:")
while (listIterator.hasPrevious()) {
    print("Index: ${listIterator.previousIndex()}")
    println(", value: ${listIterator.previous()}")
}

具有双向迭代的能力意味着 ListIterator 在到达最后一个元素后仍可以使用。

可变迭代器

为了迭代可变集合,于是有了 MutableIterator 来扩展 Iterator 使其具有元素删除函数 remove() 。因此,可以在迭代时从集合中删除元素。

val numbers = mutableListOf("one", "two", "three", "four") 
val mutableIterator = numbers.iterator()

mutableIterator.next()
mutableIterator.remove()    
println("After removal: $numbers")

除了删除元素, MutableListIterator 还可以在迭代列表时插入和替换元素。

val numbers = mutableListOf("one", "four", "four") 
val mutableListIterator = numbers.listIterator()

mutableListIterator.next()
mutableListIterator.add("two")
mutableListIterator.next()
mutableListIterator.set("three")   
println(numbers)

区间与数列

Kotlin 可通过调用 kotlin.ranges 包中的 rangeTo() 函数及其操作符形式的 .. 轻松地创建两个值的区间。 通常,rangeTo() 会辅以 in!in 函数。

if (i in 1..4) {  // 等同于 1 <= i && i <= 4
    print(i)
}

整数类型区间(IntRange、LongRange、CharRange)还有一个拓展特性:可以对其进行迭代。 这些区间也是相应整数类型的等差数列。 这种区间通常用于 for 循环中的迭代。

for (i in 1..4) print(i)

要反向迭代数字,请使用 downTo 函数而不是 ..

for (i in 4 downTo 1) print(i)

也可以通过任意步长(不一定为 1 )迭代数字。 这是通过 step 函数完成的。

for (i in 1..8 step 2) print(i)
println()
for (i in 8 downTo 1 step 2) print(i)

要迭代不包含其结束元素的数字区间,请使用 until 函数:

for (i in 1 until 10) {       // i in [1, 10), 10被排除
    print(i)
}

区间

区间从数学意义上定义了一个封闭的间隔:它由两个端点值定义,这两个端点值都包含在该区间内。 区间是为可比较类型定义的:具有顺序,可以定义任意实例是否在两个给定实例之间的区间内。 区间的主要操作是 contains,通常以 in!in 操作符的形式使用。

要为类创建一个区间,请在区间起始值上调用 rangeTo() 函数,并提供结束值作为参数。 rangeTo() 通常以操作符 .. 形式调用。

val versionRange = Version(1, 11)..Version(1, 30)
println(Version(0, 9) in versionRange)
println(Version(1, 20) in versionRange)

数列

如上个示例所示,整数类型的区间(例如 IntLongChar)可视为等差数列。 在 Kotlin 中,这些数列由特殊类型定义:IntProgressionLongProgressionCharProgression

数列具有三个基本属性:first 元素、last 元素和一个非零的 step。 首个元素为 first,后续元素是前一个元素加上一个 step。 以确定的步长在数列上进行迭代等效于 Java/JavaScript 中基于索引的 for 循环。

for (int i = first; i <= last; i += step) {
  // ……
}

通过迭代数列隐式创建区间时,此数列的 firstlast 元素是区间的端点, step 为 1 。

for (i in 1..10) 
    print(i)

要指定数列步长,请在区间上使用 step 函数。

for (i in 1..8 step 2) 
    print(i)

数列的 last 元素是这样计算的:

  • 对于正步长:不大于结束值且满足 (last - first) % step == 0 的最大值。
  • 对于负步长:不小于结束值且满足 (last - first) % step == 0 的最小值。

因此,last 元素并非总与指定的结束值相同。

for (i in 1..9 step 3) 
    print(i) // 最后一个元素是 7

要创建反向迭代的数列,请在定义其区间时使用 downTo 而不是 ..

for (i in 4 downTo 1) 
    print(i)

数列实现 Iterable<N>,其中 N 分别是 IntLongChar,因此可以在各种集合函数(如 mapfilter 与其他)中使用它们。

println((1..10).filter { it % 2 == 0 })

序列

除了集合之外,Kotlin 标准库还包含另一种容器类型——序列(Sequence<T>)。 序列提供与 Iterable 相同的函数,但实现另一种方法来进行多步骤集合处理。

Iterable 的处理包含多个步骤时,它们会优先执行:每个处理步骤完成并返回其结果——中间集合。 在此集合上执行以下步骤。

反过来,序列的多步处理在可能的情况下会延迟执行:仅当请求整个处理链的结果时才进行实际计算。

操作执行的顺序也不同:Sequence 对每个元素逐个执行所有处理步骤。 反过来,Iterable 完成整个集合的每个步骤,然后进行下一步。

因此,这些序列可避免生成中间步骤的结果,从而提高了整个集合处理链的性能。

但是,序列的延迟性质增加了一些开销,这些开销在处理较小的集合或进行更简单的计算时可能很重要。 因此,应该同时考虑使用 SequenceIterable,并确定在哪种情况更适合。

构造一个序列,主要有以下方法

由元素

要创建一个序列,请调用 sequenceOf() 函数,列出元素作为其参数。

val numbersSequence = sequenceOf("four", "three", "two", "one")

由 Iterable

如果已经有一个 Iterable 对象(例如 List 或 Set),则可以通过调用 asSequence() 从而创建一个序列。

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

由函数

创建序列的另一种方法是通过使用计算其元素的函数来构建序列。 要基于函数构建序列,请以该函数作为参数调用 generateSequence()。 (可选)可以将第一个元素指定为显式值或函数调用的结果。 当提供的函数返回 null 时,序列生成停止。 因此,以下示例中的序列是无限的。

val oddNumbers = generateSequence(1) { it + 2 } // `it` 是上一个元素
println(oddNumbers.take(5).toList())
//println(oddNumbers.count())     // 错误:此序列是无限的。

要使用 generateSequence() 创建有限序列,请提供一个函数,该函数在需要的最后一个元素之后返回 null。

val oddNumbersLessThan10 = generateSequence(1) { if (it + 2 < 10) it + 2 else null }
println(oddNumbersLessThan10.count())

由组块

最后,有一个函数可以逐个或按任意大小的组块生成序列元素——sequence() 函数。 此函数采用一个 lambda 表达式,其中包含 yield()yieldAll() 函数的调用。

它们将一个元素返回给序列使用者,并暂停 sequence() 的执行,直到使用者请求下一个元素。 yield() 使用单个元素作为参数;yieldAll() 中可以采用 Iterable 对象、Iterable 或其他 Sequence

yieldAll()Sequence 参数可以是无限的。 当然,这样的调用必须是最后一个:之后的所有调用都永远不会执行。

val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

序列操作

关于序列操作,根据其状态要求可以分为以下几类:

  • 无状态 操作不需要状态,并且可以独立处理每个元素,例如 map() 或 filter()。 无状态操作还可能需要少量常数个状态来处理元素,例如 take() 与 drop()。
  • 有状态 操作需要大量状态,通常与序列中元素的数量成比例。

如果序列操作返回延迟生成的另一个序列,则称为 中间序列。 否则,该操作为 末端 操作。 末端操作的示例为 toList()sum()。 只能通过末端操作才能检索序列元素。

序列可以多次迭代;但是,某些序列实现可能会约束自己仅迭代一次。其文档中特别提到了这一点。

序列处理示例

Iterable 假定有一个单词列表。下面的代码过滤长于三个字符的单词,并打印前四个单词的长度。

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)

运行此代码时,会看到 filter()map() 函数的执行顺序与代码中出现的顺序相同。 首先,将看到 filter:对于所有元素,然后是 length:对于在过滤之后剩余的元素,然后是最后两行的输出。 列表处理如下图:

Kotlin集合的创建与遍历

Sequence 现在用序列写相同的逻辑:

val words = "The quick brown fox jumps over the lazy dog".split(" ")
// 将列表转换为序列
val wordsSequence = words.asSequence()

val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars")
// 末端操作:以列表形式获取结果。
println(lengthsSequence.toList())

此代码的输出表明,仅在构建结果列表时才调用 filter() 与 map() 函数。 因此,首先看到文本 “Lengths of..” 的行,然后开始进行序列处理。 请注意,对于过滤后剩余的元素,映射在过滤下一个元素之前执行。 当结果大小达到 4 时,处理将停止,因为它是 take(4) 可以返回的最大大小。

序列处理如下图:

Kotlin集合的创建与遍历

在此示例中,序列处理需要 18 个步骤,而不是 23 个步骤来执行列表操作。

评论区

索引目录