Scala 集合的一个巨大优势是,它们开箱即用,包含几十种方法,并且这些方法始终可以在不可变和可变集合类型中使用。这样做的好处是,您不再需要每次使用集合时都编写自定义 for 循环,并且当您从一个项目迁移到另一个项目时,您会发现使用这些相同的方法,而不是更多自定义 for 循环。
有几十种方法可供您使用,因此此处并未全部显示。相反,仅显示一些最常用的方法,包括
mapfilterforeachheadtailtake、takeWhiledrop、dropWhilereduce
以下方法适用于所有序列类型,包括 List、Vector、ArrayBuffer 等,但这些示例使用 List,除非另有说明。
作为一个非常重要的注意事项,
List上的任何方法都不会改变列表。它们都以函数式风格工作,这意味着它们会返回一个包含修改结果的新集合。
常用方法示例
为了让你概览一下在以下部分中将看到的内容,这些示例展示了一些最常用的集合方法。首先,这里有一些不使用 lambda 的方法
val a = List(10, 20, 30, 40, 10) // List(10, 20, 30, 40, 10)
a.distinct // List(10, 20, 30, 40)
a.drop(2) // List(30, 40, 10)
a.dropRight(2) // List(10, 20, 30)
a.head // 10
a.headOption // Some(10)
a.init // List(10, 20, 30, 40)
a.intersect(List(19,20,21)) // List(20)
a.last // 10
a.lastOption // Some(10)
a.slice(2,4) // List(30, 40)
a.tail // List(20, 30, 40, 10)
a.take(3) // List(10, 20, 30)
a.takeRight(2) // List(40, 10)
高阶函数和 lambda
接下来,我们将展示一些常用的接受 lambda(匿名函数)的高阶函数 (HOF)。首先,这里有几种 lambda 语法的变体,从最长的形式开始,逐步过渡到最简洁的形式
// these functions are all equivalent and return
// the same data: List(10, 20, 10)
a.filter((i: Int) => i < 25) // 1. most explicit form
a.filter((i) => i < 25) // 2. `Int` is not required
a.filter(i => i < 25) // 3. the parens are not required
a.filter(_ < 25) // 4. `i` is not required
在那些编号的示例中
- 第一个示例展示了最长的形式。这种冗长性很少需要,并且仅在最复杂的使用情况下需要。
- 编译器知道
a包含Int,因此这里不需要重新声明这一点。 - 当你只有一个参数(例如
i)时,不需要括号。 - 当你只有一个参数,并且它只在匿名函数中出现一次时,你可以用
_替换该参数。
匿名函数 提供了更多详细信息和示例,说明了与缩短 lambda 表达式相关的规则。
现在你已经看到了简洁的形式,这里有一些使用短形式 lambda 语法的其他 HOF 示例
a.dropWhile(_ < 25) // List(30, 40, 10)
a.filter(_ > 100) // List()
a.filterNot(_ < 25) // List(30, 40)
a.find(_ > 20) // Some(30)
a.takeWhile(_ < 30) // List(10, 20)
需要注意的是,HOF 也接受方法和函数作为参数,而不仅仅是 lambda 表达式。这里有一些 map HOF 的示例,它使用名为 double 的方法。再次展示了 lambda 语法的几种变体
def double(i: Int) = i * 2
// these all return `List(20, 40, 60, 80, 20)`
a.map(i => double(i))
a.map(double(_))
a.map(double)
在最后一个示例中,当匿名函数包含一个采用单个参数的函数调用时,你不需要命名该参数,因此甚至不需要 _。
最后,你可以根据需要组合 HOF 来解决问题
// yields `List(100, 200)`
a.filter(_ < 40)
.takeWhile(_ < 30)
.map(_ * 10)
示例数据
以下部分中的示例使用这些列表
val oneToTen = (1 to 10).toList
val names = List("adam", "brandy", "chris", "david")
map
map 方法逐个遍历现有列表中的每个元素,将你提供的函数逐个应用于每个元素;然后它返回一个包含所有修改元素的新列表。
以下是将 map 方法应用于 oneToTen 列表的示例
scala> val doubles = oneToTen.map(_ * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
你还可以使用长形式编写匿名函数,如下所示
scala> val doubles = oneToTen.map(i => i * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
但是,在本课中,我们始终使用第一个较短的形式。
以下是将 map 方法应用于 oneToTen 和 names 列表的几个示例
scala> val capNames = names.map(_.capitalize)
capNames: List[String] = List(Adam, Brandy, Chris, David)
scala> val nameLengthsMap = names.map(s => (s, s.length)).toMap
nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david -> 5)
scala> val isLessThanFive = oneToTen.map(_ < 5)
isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)
如最后两个示例所示,使用 map 返回与原始类型不同的类型的集合是完全合法的(也是常见的)。
filter
filter 方法创建一个包含满足所提供谓词的元素的新列表。谓词或条件是一个返回 Boolean(true 或 false)的函数。以下是一些示例
scala> val lessThanFive = oneToTen.filter(_ < 5)
lessThanFive: List[Int] = List(1, 2, 3, 4)
scala> val evens = oneToTen.filter(_ % 2 == 0)
evens: List[Int] = List(2, 4, 6, 8, 10)
scala> val shortNames = names.filter(_.length <= 4)
shortNames: List[String] = List(adam)
集合上的函数式方法的一大优点是,你可以将它们链接在一起以解决问题。例如,此示例展示了如何链接 filter 和 map
oneToTen.filter(_ < 4).map(_ * 10)
REPL 显示结果
scala> oneToTen.filter(_ < 4).map(_ * 10)
val res1: List[Int] = List(10, 20, 30)
foreach
foreach 方法用于循环处理集合中的所有元素。请注意,foreach 用于副作用,例如打印信息。以下是有 names 列表的示例
scala> names.foreach(println)
adam
brandy
chris
david
head
head 方法来自 Lisp 和其他早期的函数式编程语言。它用于访问列表的第一个元素(头元素)
oneToTen.head // 1
names.head // adam
由于 String 可以看作是一系列字符,因此你也可以将其视为列表。这是 head 在这些字符串上工作的方式
"foo".head // 'f'
"bar".head // 'b'
head 是一个很棒的方法,但需要注意的是,当在空集合上调用它时,它也会抛出异常
val emptyList = List[Int]() // emptyList: List[Int] = List()
emptyList.head // java.util.NoSuchElementException: head of empty list
因此,你可能希望使用 headOption 而不是 head,尤其是在以函数式风格编程时
emptyList.headOption // None
如所示,它不会抛出异常,它只是返回类型 Option,其值为 None。你可以在 函数式编程 章节中了解有关此编程风格的更多信息。
tail
tail 方法也来自 Lisp,它用于打印列表中头元素之后的每个元素。几个示例演示了这一点
oneToTen.head // 1
oneToTen.tail // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
names.head // adam
names.tail // List(brandy, chris, david)
就像 head 一样,tail 也适用于字符串
"foo".tail // "oo"
"bar".tail // "ar"
如果列表为空,tail 会抛出 java.lang.UnsupportedOperationException,因此,就像 head 和 headOption 一样,还有一个 tailOption 方法,在函数式编程中更受欢迎。
列表也可以匹配,因此你可以编写类似这样的表达式
val x :: xs = names
将该代码放入 REPL 中显示 x 被分配到列表的头部,而 xs 被分配到尾部
scala> val x :: xs = names
val x: String = adam
val xs: List[String] = List(brandy, chris, david)
这种模式匹配在许多情况下很有用,例如使用递归编写 sum 方法
def sum(list: List[Int]): Int = list match {
case Nil => 0
case x :: xs => x + sum(xs)
}
def sum(list: List[Int]): Int = list match
case Nil => 0
case x :: xs => x + sum(xs)
take、takeRight、takeWhile
take、takeRight 和 takeWhile 方法为你提供了一种很好的方式来“获取”你想要用来创建新列表的列表中的元素。这是 take 和 takeRight
oneToTen.take(1) // List(1)
oneToTen.take(2) // List(1, 2)
oneToTen.takeRight(1) // List(10)
oneToTen.takeRight(2) // List(9, 10)
注意这些方法如何处理“边缘”情况,即我们要求的元素多于序列中的元素,或要求零个元素
oneToTen.take(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0) // List()
oneToTen.takeRight(0) // List()
这是 takeWhile,它使用谓词函数
oneToTen.takeWhile(_ < 5) // List(1, 2, 3, 4)
names.takeWhile(_.length < 5) // List(adam)
drop、dropRight、dropWhile
drop、dropRight 和 dropWhile 本质上与其“take”对应项相反,从列表中删除元素。以下是一些示例
oneToTen.drop(1) // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5) // List(6, 7, 8, 9, 10)
oneToTen.dropRight(8) // List(1, 2)
oneToTen.dropRight(7) // List(1, 2, 3)
再次注意这些方法如何处理边缘情况
oneToTen.drop(Int.MaxValue) // List()
oneToTen.dropRight(Int.MaxValue) // List()
oneToTen.drop(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
这是 dropWhile,它使用谓词函数
oneToTen.dropWhile(_ < 5) // List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris") // List(chris, david)
reduce
当你听到术语“map reduce”时,“reduce”部分指的是 reduce 等方法。它采用一个函数(或匿名函数)并将该函数应用于列表中的连续元素。
解释 reduce 的最佳方法是创建一个你可以传递给它的助手方法。例如,这是一个 add 方法,它将两个整数相加,还为我们提供了一些不错的调试输出
def add(x: Int, y: Int): Int = {
val theSum = x + y
println(s"received $x and $y, their sum is $theSum")
theSum
}
def add(x: Int, y: Int): Int =
val theSum = x + y
println(s"received $x and $y, their sum is $theSum")
theSum
给定该方法和此列表
val a = List(1,2,3,4)
当你将 add 方法传递到 reduce 中时,就会发生这种情况
scala> a.reduce(add)
received 1 and 2, their sum is 3
received 3 and 3, their sum is 6
received 6 and 4, their sum is 10
res0: Int = 10
正如该结果所示,reduce 使用 add 将列表 a 缩减为单个值,在本例中,即列表中整数的总和。
一旦习惯了 reduce,就可以像这样编写“求和”算法
scala> a.reduce(_ + _)
res0: Int = 10
类似地,“乘积”算法如下所示
scala> a.reduce(_ * _)
res1: Int = 24
关于
reduce需要了解的一个重要概念是,顾名思义,它用于将集合缩减为单个值。
更多
Scala 集合类型中有几十种其他方法,可以让你永远不需要再编写另一个 for 循环。请参阅 可变和不可变集合 和 Scala 集合的架构,以获取有关 Scala 集合的更多详细信息。
最后一点,如果你在 Scala 项目中使用 Java 代码,则可以将 Java 集合转换为 Scala 集合。通过这样做,你可以在
for表达式中使用这些集合,还可以利用 Scala 的函数式集合方法。有关更多详细信息,请参阅 与 Java 交互 部分。