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 表达式。
下面是一些使用名为 double
的方法的map
HOF 示例。
再次显示了 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)
但是,在本课中,我们将始终使用第一种较短的形式。
以下是更多应用于 oneToTen
和 names
列表的 map
方法的示例:
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
如图所示,它不会抛出异常,它只是返回值为 None
的类型 Option
。
您可以在 函数式编程 章节中了解有关这种编程风格的更多信息。
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
本质上与它们对应的“取”相反,从列表中删除元素。
这里有些例子:
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 交互 部分了解更多详细信息。
Contributors to this page:
Contents
- 导言
- Scala 3 特性
- 为什么是 Scala 3 ?
- Scala 的味道
- Hello, World!
- The REPL
- 变量和数据类型
- 控制结构
- 领域建模
- 方法
- 头等函数
- 单例对象
- 集合
- 上下文抽象
- 顶层定义
- 总结
- 类型初探
- 字符串插值
- 控制结构
- 领域建模
- 工具
- OOP 领域建模
- 函数式领域建模
- 方法
- 方法特性
- main 方法
- 总结
- 函数
- 匿名函数
- 函数变量
- Eta 扩展
- 高阶函数
- 自定义 map 函数
- 创建可以返回函数的方法
- 总结
- 打包和导入
- Scala 集合
- 集合类型
- 集合方法
- 总结
- 函数式编程
- 什么是函数式编程?
- 不可变值
- 纯函数
- 函数是值
- 函数式错误处理
- 总结
- 类型和类型系统
- 类型推断
- 泛型
- 相交类型
- 联合类型
- 代数数据类型
- 型变
- 不透明类型
- 结构化类型
- 依赖函数类型
- 其他类型
- 上下文抽象
- 扩展方法
- Given 实例和 Using 语句
- 上下文绑定
- Given 导入
- 实现类型类
- 多元相等性
- 隐式转换
- 总结
- 并发
- Scala 工具
- 使用 sbt 构建和测试 Scala 项目
- worksheet
- 与 Java 交互
- 向 Java 开发者介绍Scala
- Scala for JavaScript Developers
- Scala for Python Developers
- 下一步去哪