列表
List
是一个容器,即一个可以容纳其他对象的对象。
容器也称为集合。在本书的示例中,当我们需要一个基本的容器时,通常会使用 List
。
List
是 Kotlin 标准库的一部分,因此不需要进行 import
。
以下示例通过调用标准库函数 listOf()
并提供初始化值,创建了一个包含 Int
的 List
:
// Lists/Lists.kt
import atomictest.eq
fun main() {
val ints = listOf(99, 3, 5, 7, 11, 13)
ints eq "[99, 3, 5, 7, 11, 13]" // [1]
// 遍历 List 中的每个元素:
var result = ""
for (i in ints) { // [2]
result += "$i "
}
result eq "99 3 5 7 11 13"
// 对 List 进行 "索引":
ints[4] eq 11 // [3]
}
- [1]
List
在显示自己时使用方括号。 - [2]
for
循环与List
配合使用很好:for(i in ints)
表示i
会依次接收ints
中的每个值。您无需声明val i
或为其指定类型;Kotlin 从上下文中知道i
是for
循环的标识符。 - [3] 方括号 索引 到
List
中。List
保持其元素的初始化顺序,并且您可以通过数字逐个选择它们。与大多数编程语言一样,Kotlin 从元素零开始索引,本例中产生值99
。因此,索引为4
产生值11
。
忘记索引从零开始会产生所谓的差一错误。在像 Kotlin 这样的语言中,我们通常不会逐个选择元素,而是通过 in
迭代整个容器。这消除了差一错误。
如果在 List
中使用超出最后一个元素的索引,Kotlin 会抛出 ArrayIndexOutOfBoundsException
:
// Lists/OutOfBounds.kt
import atomictest.*
fun main() {
val ints = listOf(1, 2, 3)
capture {
ints[3]
} contains
listOf("ArrayIndexOutOfBoundsException")
}
List
可以容纳所有不同类型的元素。这里有一个 Double
的 List
和一个 String
的 List
:
// Lists/ListUsefulFunction.kt
import atomictest.eq
fun main() {
val doubles =
listOf(1.1, 2.2, 3.3, 4.4)
doubles.sum() eq 11.0
val strings = listOf("Twas", "Brillig",
"And", "Slithy", "Toves")
strings eq listOf("Twas", "Brillig",
"And", "Slithy", "Toves")
strings.sorted() eq listOf("And",
"Brillig", "Slithy", "Toves", "Twas")
strings.reversed() eq listOf("Toves",
"Slithy", "And", "Brillig", "Twas")
strings.first() eq "Twas"
strings.takeLast(2) eq
listOf("Slithy", "Toves")
}
这显示了 List
的一些操作。注意名称“sorted”而不是“sort”。当您调用 sorted()
时,它会生成一个包含相同元素的新 List
,按排序顺序排列,但不会改变原始 List
。将其命名为“sort”意味着直接更改原始 List
(也称为原地排序)。在 Kotlin 中的整个过程中,您会看到“保留原始对象并生成新对象”的倾向。reversed()
也会生成一个新的 List
。
参数化类型
我们认为使用类型推断是一种良好的实践——它倾向于使代码更清晰,更易于阅读。但是,有时 Kotlin 抱怨无法确定要使用的类型,在其他情况下,明确性会使代码更易于理解。以下是我们如何告诉 Kotlin List
中包含的类型:
// Lists/ParameterizedTypes.kt
import atomictest.eq
fun main() {
// 类型被推断:
val numbers = listOf(1, 2, 3)
val strings =
listOf("one", "two", "three")
// 完全相同,但是显式指定类型:
val numbers2: List<Int> = listOf(1, 2, 3)
val strings2: List<String> =
listOf("one", "two", "three")
numbers eq numbers2
strings eq strings2
}
Kotlin 使用初始化值推断 numbers
包含 Int
的 List
,而 strings
包含 String
的 List
。
numbers2
和 strings2
是 numbers
和 strings
的显式类型版本,通过添加类型声明 List<Int>
和 List<String>
创建。您之前还没有看到过尖括号——它们表示类型参数,允许您说,“这个容器包含‘参数’对象”。我们将 List<Int>
读作Int
类型的 List
”。
类型参数不仅对于容器很有用,对于其他组件也很有用,但是您经常在类似容器的对象中看到它们。
返回值也可以具有类型参数:
// Lists/ParameterizedReturn.kt
package lists
import atomictest.eq
// 返回类型被推断:
fun inferred(p: Char, q: Char) =
listOf(p, q)
// 显式返回类型:
fun explicit(p: Char, q: Char): List<Char> =
listOf(p, q)
fun main() {
inferred('a', 'b')
eq "[a, b]"
explicit('y', 'z') eq "[y, z]"
}
Kotlin 为 inferred()
推断返回类型,而 explicit()
指定函数返回类型。您不能只说它返回一个 List
;Kotlin 会抱怨,因此您必须同时提供类型参数。在指定函数的返回类型时,Kotlin 强制执行您的意图。
只读和可变列表
如果您不明确表示要可变的 List
,则不会得到一个。listOf()
生成一个不具有变异函数的只读 List
。
如果您正在逐步创建 List
(即在创建时不具有所有元素),请使用 mutableListOf()
。这会生成一个可以修改的 MutableList
:
// Lists/MutableList.kt
import atomictest.eq
fun main() {
val list = mutableListOf<Int>()
list.add(1)
list.addAll(listOf(2, 3))
list += 4
list += listOf(5, 6)
list eq listOf(1, 2, 3, 4, 5, 6)
}
您可以使用 add()
和 addAll()
将元素添加到 MutableList
,或使用快捷方式 +=
,它会添加单个元素或另一个集合。由于 list
没有初始元素,因此我们必须通过在调用 mutableListOf()
中提供 <Int>
规范来告诉 Kotlin 它的类型。
MutableList
可以被视为 List
,在这种情况下它无法被修改。但是,您不能将只读 List
视为 MutableList
:
// Lists/MutListIsList.kt
package lists
import atomictest.eq
fun getList(): List<Int> {
return mutableListOf(1, 2, 3)
}
fun main() {
// getList() 生成一个只读 List:
val list = getList()
// list += 3 // Error
list eq listOf(1, 2, 3)
}
请注意,尽管 list
是在 getList()
内部使用 mutableListOf()
创建的可变对象的不可变引用(val
),但在 return
期间,结果类型变为 List<Int>
。原始对象仍然是 MutableList
,但是它通过 List
的视角来查看。
List
是只读的 — 您可以读取其内容但不能写入。如果底层实现是一个 MutableList
并且您保留了对该实现的可变引用,您仍然可以通过该可变引用修改它,并且任何只读引用都将看到这些更改。这是别名的另一个示例,介绍在限制可见性中:
// Lists/MultipleListRefs.kt
import atomictest.eq
fun main() {
val first = mutableListOf(1)
val second: List<Int> = first
second eq listOf(1)
first += 2
// second 观察到了变化:
second eq listOf(1, 2)
}
first
是对 mutableListOf(1)
生成的可变对象的一个不可变引用(val
)。然后将 second
别名设置为 first
,因此它是对同一对象的视图。second
是只读的,因为 List<Int>
不包括修改函数。注意,如果没有显式的 List<Int>
类型声明,Kotlin 将推断 second
也是对可变对象的引用。
我们能够将元素(2
)添加到对象中,因为 first
是对可变 List
的引用。请注意,second
观察到了这些更改 —— 它不能更改 List
,尽管 List
通过 first
发生了更改。
练习和解答可以在 www.AtomicKotlin.com 找到。