当你操作集合时,函数式风格提供了许多好处。你可以将库函数用于大部分任务并简化你的代码。在这一章节,我们将会讨论一些用于集合的Kotlin标准库函数。我们主要以filter和map,以及它们背后的概念为开始。我们也会覆盖其他有用的函数。我们会给你一些有关如何重用它们,以及如何编写更加清晰易读的代码的提示。
注意,其中没有一个函数是Kotlin的设计者发明的。这些或类似的函数对于所有支持lambda的语言都是可用的。如果你以及对这些概念非常熟悉了,你可以通过下面的例子快速看一眼并跳过其中的解释。
5.2.1 必要的:filter和map
filter
和map
函数组成了操作集合的基础。许多的集合请求可以在它们的帮助下进行表达。
对每一个函数,我们都将提供一个有数字和属性的Person
类的例子:
data class Person(val name: String, val age: Int)
filter
函数变换一个集合,并过滤出不满足给定断言的元素:
>>> val list = listOf(1, 2, 3, 4)
>>> list.filter { it % 2 == 0 } // 1 只保留偶数
[2, 4]
结果是只包含来自输入集合满足断言的元素的新集合。如图5.3所示:
图5.3 过滤器是如何工作的
如果你只想保留年龄大于30的人,你可以使用filter
:
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> people.filter { it.age > 30 }
[Person("Bob", 31)]
filter
函数可以从集合中移除不想要的元素。但是它不能修改元素。要修改元素,map
就起作用了。map
函数对集合中的每个元素应用给定的函数,并且收集结果形成新的集合。例如,你可以把一列数字变换为它们的平方:
>>> val list = listOf(1, 2, 3, 4)
>>> list.map { it * it }
[1, 4, 9, 16]
结果是一个包含同样的元素个数,但是每个元素根据给定的断言进行变换的集合(详见图5.4)。
图5.4 map函数的工作原理
如果你只是想打印一列名字,而不是一列人,你可以使用map
来变换列表:
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> people.map { it.name }
[Alice, Bob]
注意了,这个例子可以使用成员引用完美的重写:
people.map(Person::name)
你可以方便的将多个调用像这样串成一条链子。例如,我们打印年龄大于30的人的名字:
>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]
现在,我们假设你需要组中最年长的人的名字。你可以找出组中年纪最大的人。然后返回每一个人的年龄。这样的代码用lambda写非常简单:
people.filter { it.age == people.maxBy(Person::age).age }
但是要注意,这份代码为每个人重复了最大年龄的查找步骤。所以,如果集合中有100个人,最大年龄的搜索将会执行100次!
下面的解决方案进行了优化。它只计算一次最大年龄:
val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }
如果你没有需要,就不要重复进行计算。看上去简单的代码,使用lambda表达式,有时候可以隐藏底层操作的复杂性。所以,你总能记住你写的代码中发生了什么。
你也可以对映射应用过滤和变换函数:
>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() })
{0=ZERO, 1=ONE}
Kotlin有函数来分别操作键和值。filterKeys
和mapKeys
过滤并变换映射的键。相应的,filterValues
和mapValues
过滤和变换对应的值。
5.2.2 all, any, count
和find
:对集合应用预言
另一个常见的任务是检查集合中所有的元素是否匹配某个条件。Kotlin中,这可以通过all
和any
函数进行表达。count
函数检查多少个元素满足这个预言。find
函数返回第一个匹配的元素。
为了介绍这些函数,我们定义一个语言canBeInClub27
来检查某个人是否为27岁或者更年轻:
val canBeInClub27 = { p: Person -> p.age <= 27 }
如果你对所有的元素是否符合这个预言,你可以使用all
函数:
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false
如果你需要检查是否至少有一个匹配的元素,使用any
:
>>> println(people.any(canBeInClub27))
true
要注意,!all
(非全部)可以使用一个否定条件来替代any
,反之亦然。为了让你对代码更加容易理解,你最好选择一个不需要你在他都前面放置否定符号的函数:
>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) // 1 否定符号!并不引人注意。所以,这个案例中使用"any"更加合适
true
>>> println(list.any { it != 3 }) // 2 参数中的条件改成了相反的情况
true
第一个检查确保并非所有元素都等于3。它等价于至少有一个不等于3的元素。这就是你在第二行使用any
进行的检查。
如果你想知道有多少个元素符合这个预言,使用count
:
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1
SIDEBAR 为任务使用右边的函数:count vs size
你很容易忘记了count
的存在。而且你通过过滤集合实现它的功能,得到了它的大小:
>>> println(people.filter(canBeInClub27).size) 1
在这个案例中,创建了一个临时的集合来保存所有符合预言的元素。另一方面,
count
方法只追踪匹配的元素的个数,而不是元素本身。因此,它会更加高效。
作为一个通用的法则,试着去找出满足你的需求的最合适的操作。
为了找出一个符合预言的元素,请使用find
函数:
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Alice, age=27)
如果有多个元素,函数将返回第一个匹配的元素。如果没有满足的元素,函数返回null
。find
的一个同义词是firstOrNull
。如果如能够更加清晰的表达你对想法,你可以使用firstOrNull
函数。
5.2.3 groupBy
:把一个列表转换为多组映射
想象一下,你需要根据你某些特性来将所有元素分割成不同的组。例如,你想把年龄相同的人放在一组。把这个特性直接作为一个参数进行传递非常方便!groupBy
函数可以为你做到这一点:
>>> val people = listOf(Person("Alice", 31),
...
Person("Bob", 29), Person("Carol", 31))
>>> println(people.groupBy { it.age })
这个操作的结果是一个映射通过键被分成多个组。见图5.5。
图5.5 应用groupBy函数的结果
对于这个案例,输出以下结果:
{29=[Person(name=Bob, age=29)],
31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}
每一组都被保存成一个列表。所以结果的类型为Map<Int, List<Person>>
。你可以使用像mapKeys
和mapValues
这样的函数对这个映射做更多的修改。
正如另一个例子所示,我们来看看如何使用成员引用通过第一个字符对字符串进行分组:
>>> val list = listOf("a", "ab", "b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}
注意,这里的first
不是String
类的成员。它是一个扩展(函数)。不过你可以把它当做一个成员引用进行访问。
5.2.4 flatMap和flatten:处理嵌套集合中的元素
现在,让我们把有关人的讨论放到一边,切换到书的讨论。假定你有一个书库,用Book
类来表示:
class Book(val title: String, val authors: List<String>)
每一本书有一个或多个作者。你可以计算你的书库中所有作者的集合:
books.flatMap { it.authors }.toSet() // 编写书籍的所有作者的集合
flatMap
函数做了两件事:首先它根据作为参数而给定的函数把每一个元素都变换(或映射)到一个集合中。然后它把多个列表合并为一个。有一个处理字符串的案例很好的解析了这个概念(见图5.6):
>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
图5.6 应用flatMap函数后的结果
字符串的toList()
函数将其自身转换为字符的列表。如果你有把map
函数跟toList()
一起使用,如图中的第二行所示,你会得到一个嵌套在列表中的列表的字符串。flatMap
函数也做了以下步骤。之后返回一个由所有元素组成的列表。
我们来看看返回的作者:
>>> val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
Book("Mort", listOf("Terry Pratchett")),
... Book("Good Omens", listOf("Terry Pratchett",
... "Neal Gaiman")))
>>> println(books.flatMap { it.authors }.toSet())
[Jasper Fforde, Terry Pratchett, Neal Gaiman]
每一本书都由一个或多个作者编写。book.authors
属性保存了作者的集合。flatMap
函数把所有书的作者都组装进了一个扁平的(没有嵌套的)列表。toSet
调用从结果集合中移除了重复的元素。所以,在这个案例中,Terry Pratchett只会在打印输出中列举一次。
当你陷入必须合并为一个集合的嵌套集合的元素时,你可以考虑flatMap
。注意,如果你需要变换任何东西,只是仅仅需要把这样一个集合变得扁平,你可以使用flatten
函数:listOfLists.flatten()
。
我们重点讲了Kotlin标准库中的一些集合操作函数。但是里边还有更多的函数。处于篇幅的考虑,也因为展示一长串的函数非常的无聊,我们不会面面俱到。我们一般建议,当你编写用到了集合的代码时,考虑如何将操作表达为一个通用的变换。然后查找执行这个变换的库函数。你很可能找到一个这样的函数,并用它来解决你的问题。这往往比你手工实现更加快速。
现在,我们来仔细看看集合的链式操作吧!在下一章节,你将会看到操作执行的几种不同方式。