Scala 集合的一个巨大优势是,它们开箱即用,包含几十种方法,并且这些方法始终可以在不可变和可变集合类型中使用。这样做的好处是,您不再需要每次使用集合时都编写自定义 for
循环,并且当您从一个项目迁移到另一个项目时,您会发现使用这些相同的方法,而不是更多自定义 for
循环。
有几十种方法可供您使用,因此此处并未全部显示。相反,仅显示一些最常用的方法,包括
map
filter
foreach
head
tail
take
、takeWhile
drop
、dropWhile
reduce
以下方法适用于所有序列类型,包括 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 交互 部分。