到目前为止,我们只把变量当作单独的值来使用。一旦开始以集合(collection)的方式将变量组合起来工作,Kotlin 代码就会变得有趣得多!要学习集合,让我们去拜访 Libby 吧,她是一位聪明伶俐的年轻女士,总是在身边放着一本书!
谁喜欢阅读书籍?
Libby 是一位狂热的读者。她总是在寻找精彩的小说,所以每当有人向她推荐好书时,她就把书名记在一张纸上。以下是目前她列表中的书名:
Libby 在业余时间一直在学习编写 Kotlin 代码,所以她也想用 Kotlin 来写这个列表,并把所有书名打印到屏幕上。以下是她写的代码:
val book1 = "Tea with Agatha"
val book2 = "Mystery on First Avenue"
val book3 = "The Ravine of Sorrows"
val book4 = "Among the Aliens"
val book5 = "The Kingsford Manor Mystery"
println(book1)
println(book2)
println(book3)
println(book4)
println(book5)”嗯……”她想。”每次添加一本新书,我都得创建一个新变量。而且保持编号的顺序也很困难——如果我从列表中间移除book3,那么book4和book5就需要改名为book3和book4。要是有更好的方法来管理我的书名列表就好了……”
列表简介
幸运的是,这里有一个更好得多的方法!Libby 可以创建一个集合(collection)。让我们从 Kotlin 中最常见的集合类型之一开始——列表(list)。创建列表很容易——只需调用listOf(),并将你想要的值作为参数,用逗号分隔即可。让我们更新 Libby 的代码,使用列表。
val booksToRead = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)这段代码看起来和 Libby 手写的列表非常相似。事实上,让我们来比较一下两者!
手写列表和 Kotlin 列表有很多共同点:
- 首先,它们都有一个名称。在 Kotlin 中,保存列表的变量名就像是纸上的列表名称。
- 其次,两个列表中都有项目——在这个例子中是书名。在 Kotlin 中,列表中的项目称为元素(elements)。
- 最后,两个列表中的书名都有特定的顺序。
以前,我们使用println()函数将变量的内容打印到屏幕。你也可以对集合变量使用println()。
println(booksToRead)当你这样做时,你会按顺序看到它的元素,如下所示:
[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, Among the Aliens, The Kingsford Manor Mystery]
集合与类型
在 Kotlin 中处理集合时,我们需要考虑两种不同的类型。
- 我们使用的集合的类型。
- 集合中元素的类型。
这两个要素共同决定了集合变量的整体类型。以代码清单 8.2为例:
- 集合是一个
List(列表)。 - 集合中元素的类型是
String(字符串)。
一旦你知道这两点,就能很容易地写出集合变量的类型。先写集合的类型,然后在左尖括号<和右尖括号>之间写元素的类型。因此,booksToRead的类型是List<String>。
让我们重写代码清单 8.2,这次显式地包含booksToRead集合变量的类型信息。
val booksToRead: List<String> = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)这种类型是泛型(generic)的一个实例。我们将在后续章节中详细介绍泛型。现在,你只需要知道如何编写列表的类型,以防需要将它用作函数的参数类型或返回值类型。
添加和删除元素
Libby 刚从朋友 Rebecca 那听说了一本很棒的新书!她准备把这本名为《Beyond the Expanse》的新书添加到她的列表中。她该怎么做呢?
当然,她可以直接在listOf()末尾添加一个参数。但如果是在列表已经创建之后再添加书名呢?
在 Kotlin 中,一旦你调用listOf()创建了一个列表,该列表就不能再被更改。你不能向其中添加任何内容,也不能从中删除任何内容。在编程中,”更改”的雅称是变更(mutate),因此不允许添加或删除元素的列表称为不可变列表(immutable list)。
虽然你不能从常规 Kotlin List中添加或删除元素,但你可以通过将原始列表与新元素组合来创建一个新列表。要做到这一点,请使用加法运算符(plus operator)。即使用+将原始列表与新项目连接起来,并将其赋值给一个新变量,如下所示:
val booksToRead = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)
val newBooksToRead = booksToRead + "Beyond the Expanse"在这段代码中,booksToRead + "Beyond the Expanse"是一个表达式,它的计算结果是一个新的List实例。因此,当这段代码运行完毕后,我们有两个集合变量——booksToRead和newBooksToRead。
这就像是在第二张纸上写下新的书单。这样,Libby 实际上拥有了两份列表——原始列表和新列表:
你可能还记得第 1 章中的内容,变量可以用val或var来声明,包括保存集合的变量。但请注意,用var声明集合变量并不会改变列表本身不可变的事实。换句话说,仅仅用var声明它并不能让你添加或删除元素。但是,var确实允许你将另一个不可变列表赋值给它。
因此,通过将booksToRead变量从val改为var,新列表可以赋值给现有的变量名,如下所示:
var booksToRead = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)
booksToRead = booksToRead + "Beyond the Expanse"这就像是扔掉旧的纸制列表,然后直接给新列表起一个和旧列表同样的名字。
Libby 的列表现在有六本书了。就在她以为列表更新完毕时,Rebecca 又发来了消息。”你知道吗,我上周读了《Among the Aliens》。真的不太好看,”她说。”你不应该浪费时间读那本。”
Libby 想把那一本从列表中划掉。正如你可能猜到的,你可以用类似的方式从列表中删除元素,但不使用加法运算符,而是使用减法运算符(minus operator)。
var booksToRead = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)
booksToRead = booksToRead + "Beyond the Expanse"
booksToRead = booksToRead - "Among the Aliens"实际上,最后这两行可以合并成一行,如下所示:
booksToRead = booksToRead + "Beyond the Expanse" - "Among the Aliens"现在,当 Libby 执行println(booksToRead)时,她在屏幕上看到以下内容:
[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]
”太棒了!”她心想,”我的待读书单全部更新完毕了!”
List 和 MutableList
到目前为止,我们一直在使用常规的 Kotlin List,它不允许被更改,正如我们在上面看到的那样。相反,我们必须使用加法或减法运算符来创建一个新列表。
但是,Kotlin 也提供了另一种列表——一种确实允许你更改它的列表。由于这些列表允许更改,它们被称为可变列表(mutable lists),它们的类型是MutableList。
使用可变列表时,你可以使用add()和remove()函数来添加或删除元素,如下所示:
val booksToRead: MutableList<String> = mutableListOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"Among the Aliens",
"The Kingsford Manor Mystery",
)
booksToRead.add("Beyond the Expanse")
booksToRead.remove("Among the Aliens")使用可变列表有点像 Libby 用铅笔而不是钢笔来写她的纸质列表。她可以擦掉一个书名,或者添加一个新的,而不需要用另一张纸。
Libby 准备好开始阅读那些书了!但为了知道从哪本书开始阅读,她需要知道如何从列表中获取单个书名。
从列表中获取元素
”好吧,我列表上的第一本书是什么?”Libby 思考着。她低头看了看手写的页面。她很容易看出哪本是第一本。”Tea with Agatha,”她记下来了。”现在我如何在 Kotlin 中从列表里获取第一本书呢?”
如前所述,列表中的元素按照特定顺序排列,这个顺序对于从列表中获取单个元素非常重要。下面是它的工作原理:
列表中的每个元素都会被赋予一个数字,称为索引(index),这个数字基于元素在列表中的位置。第一个元素的索引是0,第二个是1,第三个是2,以此类推。
一旦知道索引,从列表中获取元素就很容易了。只需调用get()成员函数,将索引作为参数传入。例如,Libby 可以通过如下方式调用get(0)来获取列表中的第一个元素:
val booksToRead = listOf(
"Tea with Agatha",
"Mystery on First Avenue",
"The Ravine of Sorrows",
"The Kingsford Manor Mystery",
"Beyond the Expanse"
)
val firstBook = booksToRead.get(0)
println(firstBook) // Tea with Agatha”太棒了!”Libby 说。”现在我可以轻松地从书单中获取单个书名了!”
除了直接调用get()函数,你还可以使用索引访问运算符(indexed access operator),它用左括号[和右括号]表示,中间是索引。下列代码清单中的代码与上面代码的功能完全相同。
val firstBook = booksToRead[0]
println(firstBook) // Tea with AgathaKotlin 开发者使用索引访问运算符的频率比使用get()函数高得多,所以我们之后将使用索引访问运算符。
现在,从列表中获取单个元素可能很有帮助,但当我们想要对列表中的每个元素执行某种操作时,集合就变得特别有用了。让我们接下来看看如何做到这一点!
循环和迭代
”现在,我想把书单打印到屏幕上,”Libby 自言自语道。”我用println(booksToRead)来实现!” 运行代码后,她看到了以下输出:
[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]
”虽然能这么容易地打印列表很好,但我真的想像手写列表那样竖着看列表。”
她心中设想的是这样的:
Tea with Agatha
Mystery on First Avenue
The Ravine of Sorrows
The Kingsford Manor Mystery
Beyond the Expanse
当然,要实现这一点,她可以逐一地对每个元素调用println(),如下所示:
println(booksToRead[0])
println(booksToRead[1])
println(booksToRead[2])
println(booksToRead[3])
println(booksToRead[4])但是,像这样编写代码相当乏味。此外,如果按错误顺序打印元素,或者意外多次打印同一元素,很容易出错。实际上,这看起来很像代码清单 8.1中的代码!
不用为列表中的每个元素逐一编写相同的代码,如果 Kotlin 可以遍历每个元素,一个接一个地对其调用 println(),那会怎么样呢?
幸运的是,这在 Kotlin 中非常容易实现!我们可以使用 forEach() 函数。下面是它的用法。
booksToRead.forEach { element ->
println(element)
}当 Kotlin 运行这段代码时,它会先对第一个元素执行 println(element),然后回过头来再对第二个元素执行它,然后再回过头来对第三个元素执行它,以此类推。通过反复执行这行代码,就好像在循环转圈一样,就像这样:
这就是为什么编程语言称其为 循环(loop)——因为对于集合中的每个元素,它都会循环回到那段代码。它通常也被称为 迭代(iterating),每次运行代码时,都被称为一次 迭代(iteration)。
让我们仔细看看 forEach(),以理解为什么我们要以这种方式构建代码。
forEach() 是存在于集合变量上的成员函数。它是一个接受高阶函数(higher-order function),该函数接受一个lambda 表达式。这个 lambda 表达式就是你希望 Kotlin 对集合中的每个元素运行的代码。
这里我们将参数命名为 element,但你也可以将其命名为 title。或者,由于这个 lambda 只有一个参数,你可以使用隐式 ‘it’ 参数,这样会更加简洁。事实上,我们可以把它放在一行上:
booksToRead.forEach { println(it) }无论哪种情况,结果正是 Libby 想要的——书名被垂直打印出来,就像在她的纸质记事本上一样!
Tea with Agatha
Mystery on First Avenue
The Ravine of Sorrows
The Kingsford Manor Mystery
Beyond the Expanse
集合操作
Libby 准备把她感兴趣的书单分享给其他感兴趣的人,从她的朋友 Nolan 开始。但是,当她为他制作列表副本时,她想对一些书名进行修改。
”我真的想去掉每个书名开头的’The’这个词,”Libby 想。”这样我就能按字母顺序排序了,而且以’The’开头的书名就不会挤在一起了。”
映射集合:转换元素
有时当你从现有集合创建一个新集合时,你也希望以某种方式转换其中一个或多个元素。在 Libby 的例子中,她想删除书名开头的”The”,以便它们可以用于排序。
在对所有书名进行这种转换之前,让我们先从其中一个开始。String 对象有一个 removePrefix() 函数,你可以使用它来删除字符串开头的单词。以下是它的用法:
val sortableTitle = "The Kingsford Manor Mystery".removePrefix("The ")
println(sortableTitle) // Kingsford Manor Mystery完美!现在她只需要将这个 removePrefix() 函数应用到列表中的每个元素上即可!
”也许我可以使用 forEach(),因为我知道它对列表中的每个元素进行操作”,Libby 想。她撸起袖子,写出了以下代码:
val sortableTitles: MutableList = mutableListOf()
booksToRead.forEach { title ->
sortableTitles.add(title.removePrefix("The "))
}
sortableTitles.forEach { println(it) } ”嗯,这个方法可行,”Libby 想。”但有点复杂,而且要写的代码很多……”
这之所以复杂,是因为 Libby 想要创建一个新集合,但 forEach() 做不到这一点。它只是对现有集合运行 lambda,然后返回 Unit。她真正需要的是一个能运行 lambda 并将 lambda 的结果作为元素包含在新集合中的集合操作。
在 Kotlin 中,这个集合操作叫做 map()。下面是 Libby 如何使用它来从新集合中书名的开头移除”The”这个词:
val sortableTitles = booksToRead.map { title ->
title.removePrefix("The ")
}这段代码的作用与前一个代码清单相同(只是结果是一个不可变的 List 而不是 MutableList)。与 forEach() 一样,map() 函数对列表中的每个元素调用一次 lambda。然而,与 forEach() 不同的是,map() 会在每次迭代中使用 lambda 的结果来构建一个新列表。
当你打印列表中的每个元素时,你可以看到 The Ravine of Sorrows 和 The Kingsford Manor Mystery 都已被更新,使得”The”这个词不再位于开头。
Tea with Agatha
Mystery on First Avenue
Ravine of Sorrows
Kingsford Manor Mystery
Beyond the Expanse
让我们仔细看看 map() 函数:
- 与
forEach()类似,map()函数是一个接受 lambda 的高阶函数。 - 该 lambda 将对列表中的每个元素运行一次。
- lambda 的结果将成为新集合中的一个元素。
map()函数返回该新集合。
forEach()和map()这样的函数被称为集合操作(collection operations),因为它们是对集合执行某种操作的函数。
”完美!”Libby 说,”既然书名已经按我想要的方式更改了,也许我可以给它们排序?”
排序集合
forEach()和map()函数只是 Kotlin 中众多集合操作中的两个。另一个非常有用的函数叫做sorted()。
由于 map() 函数返回一个集合,Libby 可以在调用 map() 之后直接调用 sorted(),如下所示:
val sortedTitles = booksToRead.map { title -> title.removePrefix("The ") }.sorted()当她打印出 sortedTitles 的元素时,她看到了她希望看到的输出!
Beyond the Expanse
Kingsford Manor Mystery
Mystery on First Avenue
Ravine of Sorrows
Tea with Agatha
为了让代码更易于阅读,每个集合操作可以单独成行,如下所示:
val sortedTitles = booksToRead
.map { title -> title.removePrefix("The ") }
.sorted()此代码与前一个代码清单相同,只是格式不同。换句话说,所有的字母和标点符号完全相同且顺序一致——只是它们之间的空格不同。
像这样垂直书写集合操作会很有帮助,因为它可以让你很容易地向下扫视各行,看看涉及哪些集合操作以及它们的顺序。例如,首先对书名进行映射,然后对书名进行排序。因此,Kotlin 开发者经常这样格式化他们的代码。
过滤集合:包含和省略元素
Libby 非常兴奋!现在她有一份按字母顺序排列的书单,可以分享给 Nolan 了。
”我迫不及待想看到你的书单了,”Nolan 说。”记住——我只读推理小说!”
”只要推理小说……?”Libby 重复道。”好吧,”她心里想,”我最后一件事就是要删除不是推理小说的书名。”她从笔记本上撕下一页,只为 Nolan 写了一份定制列表,省略了所有不是推理小说的书名。
”我如何在 Kotlin 中做到这一点?”她想知道。
你可能已经猜到了,Kotlin 包含一个使这变得容易的集合操作,而且毫不意外,它叫做 filter()。
就像空气过滤器阻止灰尘和过敏原进入你的空调系统一样,Kotlin 列表过滤器会阻止你不希望进入新列表的元素!
让我们使用 filter() 函数来筛选列表中的书籍,只保留标题中包含”Mystery”的书籍:
val booksForNolan = booksToRead
.map { title -> title.removePrefix("The ") }
.sorted()
.filter { title -> title.contains("Mystery") }filter() 函数与上面的 map() 函数类似——它接受一个 lambda 作为参数,并且该 lambda 将对原始列表中的每个标题调用一次。然而,与 map() 函数不同的是,filter() 的 lambda 必须返回一个 Boolean。如果它对某个元素返回 true,则该元素会被传入新集合(即本例中的 booksForNolan)。如果它返回 false,则该元素会被省略,不出现在新集合中。
下面是 filter() 函数用法的详细分解:
打印列表中的每个元素,Libby 看到的结果如下:
Kingsford Manor Mystery
Mystery on First Avenue
”很好,”她说。”列表正是我想要的样子。它只包含推理小说,而且排序正确!”
集合操作链
让我们再看一遍那段代码:
val booksForNolan = booksToRead
.map { title -> title.removePrefix("The ") }
.sorted()
.filter { title -> title.contains("Mystery") }在 Kotlin 中,像这样将多个集合操作一个接一个地放在一起是很常见的。当我们这样做时,这被称为链式调用(chaining)集合操作——每个操作就像是链条中的一环。在这个代码清单中,map()、sorted() 和 filter() 调用被链接在一起。
请记住,操作链不是在更改单个列表。事实上,这些操作中的每一个都会创建一个新列表。最终操作 filter() 创建的列表被赋值给变量 booksForNolan。中间列表——即链内部的集合操作创建的列表——被链中的下一个操作使用,但没有赋值给任何变量。不过,记住这些中间列表仍然很重要。下面的插图显示了链中每一步涉及的列表。
当你遇到这样的集合操作链时,考虑每个中间列表中有多少元素会很有帮助。例如,代码清单 8.21 中的代码将 filter() 调用放在链的末尾。但如果它放在链的开头会怎样呢?就像这样:
val booksForNolan = booksToRead
.filter { title -> title.contains("Mystery") }
.map { title -> title.removePrefix("The ") }
.sorted()通过这样做,filter() 产生的中间列表只有两个元素,在这种情况下,map() 函数只需要调用其 lambda 两次而不是五次,sorted() 也只需要排序两个元素而不是五个。在这个例子中,无论哪种方式,最终列表都是相同的,但 代码清单 8.22 可能比 代码清单 8.21 更高效。
下面的插图显示了将 filter() 调用放在顶部时每一步涉及的列表。请注意,中间列表的元素比前一个插图中的要少。
在这样的小列表上,这没什么大不了的,但在一个有数百或数千个元素的列表上,你可以看到这如何提高代码的性能——也就是说,它运行得更快!
其他集合操作
Kotlin 还有许多其他易于使用的集合操作!下面列出几个可能会对你有帮助的,让你先了解一下。
drop(3)- 新列表省略原始列表中的前 3 个元素。take(5)- 新列表只使用原始列表中的前 5 个元素。distinct()- 新列表将省略重复的元素,使每个元素只包含一次。reversed()- 新列表将包含与原始列表相同的元素,但顺序相反。
你可以在 Kotlin API 文档中看到更完整的列表。
集合简介
在我们结束本章之前,值得注意的是,列表并不是 Kotlin 中唯一的一种集合。列表可能是最常用的,但另一种有用的集合类型叫做集合(Set)。列表有助于确保其元素按特定顺序排列,而集合有助于确保其中的每个元素始终是唯一的。
例如,Nolan 最喜欢的推理小说作家 Slim Chancery 写了三本书,Nolan 骄傲地说他收集了完整的套装。
在 Kotlin 中创建集合和创建列表一样容易。只需使用setOf()或mutableSetOf(),而不是listOf()或mutableListOf()。
val booksBySlim: Set = setOf(
"The Malt Shop Caper",
"Who is Mrs. W?",
"At Midnight or Later",
) 当你向一个已经包含该值的集合添加元素时,集合将保持不变。
val booksBySlim: MutableSet = mutableSetOf(
"The Malt Shop Caper",
"Who is Mrs. W?",
"At Midnight or Later",
)
booksBySlim.add("The Malt Shop Caper")
println(booksBySlim)
// [The Malt Shop Caper, Who is Mrs. W?, At Midnight or Later] 请注意,当打印集合或对其使用集合操作时,集合不能保证其元素的顺序。元素的顺序可能与你添加它们的顺序相同,但不要依赖这一点!
因为集合的元素没有特定顺序,所以它们的元素没有索引。出于这个原因,集合甚至不包含 get() 函数!
关键要点是:
- 列表中的元素有保证的顺序,并且可以包含重复元素。
- 集合中的元素没有特定顺序,并且保证不包含重复元素。
另外,你可以将列表转换为集合,反之亦然。只需使用 toSet() 或 toList()。请记住,如果你将列表转换为集合,你将丢失重复元素,而且顺序可能会不同!
val bookList = listOf(
"The Malt Shop Caper",
"At Midnight or Later",
"The Malt Shop Caper",
)
val bookSet = bookList.toSet() // bookSet has two elements
val anotherBookList = bookSet.toList() // anotherBookList also has two elements总结
在本章之前,我们只处理单独的变量。通过使用列表和集合这样的集合,我们能够对整组值进行操作,这开辟了全新的可能性世界!在本章中,你学习了集合,包括:
- 如何创建列表。
- 如何通过从另一个列表添加或删除元素来创建列表。
- 不可变集合和可变集合>之间的区别。
- 如何从列表中获取单个元素。
- 集合操作,如
filter、map和sorted。 - 列表和集合之间的区别。
我们发现通过索引从列表中获取元素非常容易。然而,有时你需要一种通过其他信息获取元素的简单方法。例如,你可能想通过 ISBN(背面条形码上方的那个长数字)来获取一本书,而不是通过位置索引。在下一章中,我们将学习另一种对元素进行分组的方法,使你能轻松做到这一点!届时见!