在处理 Scala 的数据结构时,mapflatMap 是经常被调用的两个集合类的方法。

map 方法经常被作用于 collections,并且是 Traversable 特质的一个成员。

关于 TraversableIterable 的区别,可以参照这个回答

map 方法基本功能为: 遍历整个集合的每个元素,将这个集合转换为一个新的集合。
flatMap 方法的功能为: 先在整个集合上调用 map 方法,然后再在结果集合上调用 flatten 方法。

本文试图从三个使用场景上分析 mapflatMap 方法的用法:

  • Collections
  • Options
  • Futures

Collections

这是 map 最常见的使用场景:

遍历一个集合类型,在其中每个元素上执行一个操作,并将操作结果添加到一个新的集合中。

序列

例如,将一个列表中所有元素都加倍:

1
2
scala> List(1,2,3).map { x => x * 2 }
List[Int] = List(2, 4, 6)

或者使用下划线_快捷方法:

1
2
scala> List(1,2,3).map { _ * 2 }
List[Int] = List(2, 4, 6)

其他集合类型的使用方法类似:

1
2
3
4
5
6
scala> Array(1,2,3).map(_*2)
Array[Int] = Array(2, 4, 6)
scala> Set(1,2,2,3).map(_*2)
Set[Int] = Set(2, 4, 6)
scala> (0 until 5).map(_*2)
IndexedSeq[Int] = Vector(0, 2, 4, 6, 8)

Map 类型

注意,Map 类型也有 map 方法,不同的是,map 方法会将每个 key-value 键值对转换成一个 tuple ,然后将 map 方法中的操作作用于这个 tuple:

1
2
3
4
scala> Map("key1" -> 1, "key2" -> 2).map { keyValue:(String,Int) =>
keyValue match { case (key, value) => (key, value*2) }
}
Map[String,Int] = Map(key1 -> 2, key2 -> 4)

由于上述的匿名函数只是将输入元素传递给一个模式匹配器,Scala 允许我们只传递一个带有 tuple 提取器case 声明

1
2
3
4
scala> Map("key1" -> 1, "key2" -> 2).map {
case (key, value) => (key, value*2)
}
Map[String,Int] = Map(key1 -> 2, key2 -> 4)

除了被转换成 Map 类型,也可以转换成其他类型:

1
2
3
4
5
6
7
8
scala> Map("key1" -> 1, "key2" -> 2).map {
case (key, value) => value * 2
}
Iterable[Int] = List(2, 4)
scala> Map("key1" -> 1, "key2" -> 2).map {
case (key, value) => value * 2
}.toSet
Set[Int] = Set(2, 4)

String 类型

由于 String 类型可以被当做 Char 类型的集合,所以也可以在其上使用 map 方法:

1
2
scala> "Hello".map { _.toUpper }
String = HELLO

flatMap

Scala 集合类型还支持 flatten 方法,该该方法通常用于消除不想要的集合嵌套。

1
2
scala> List(List(1,2,3),List(4,5,6)).flatten
List[Int] = List(1, 2, 3, 4, 5, 6)

flatMap 方法的作用就是: 先在集合类型上使用 map 方法,然后立即在其结果上调用 flatten 方法。

1
2
3
4
scala> List(1,4,9).flatMap { x => List(x,x+1) }
List[Int] = List(1, 2, 4, 5, 9, 10)
scala> List(1,4,9).flatMap { x => if (x > 5) List() else List(x) }
List[Int] = List(1, 4)

mapflatMap 方法真正的用武之地在 OptionFuture 类型上。

Option

虽然 Scala 的 Option 不是集合类型,但也是支持 mapflatMap 方法的。

  • 当作用在 Some 类型时,map 方法会作用在真实值上:
    例如,Some(1) 会作用在 1 上。

  • 而作用在 None 上时,会直接返回一个 None

1
2
3
4
scala> val fee = 1.25
scala> val cost = Some(4.50)
scala> val finalCost = cost.map(_+fee)
finalCost: Option[Double] = Some(5.75)

而如果 cost 没有值:

1
2
3
scala> val cost:Option[Double] = None
scala> val finalCost = cost.map(_+fee)
finalCost: Option[Double] = None

Option 场景下,flatten 方法会消除嵌套 Option:

1
2
3
4
5
6
scala> Some(Some(1)).flatten
Option[Int] = Some(1)
scala> Some(None).flatten
Option[Nothing] = None
scala> None.flatten
Option[Nothing] = None

Option 和集合类型结合在一起时,flattenflatMap 允许我们操控并将复杂的结果变得更加可控:

1
2
scala> List(Some(1),Some(2),None,Some(4),None).flatten
List[Int] = List(1, 2, 4)

Future

Future 是 Scala 中实现并发的标准方法。通常情况下,我们通过使用回调函数来决定当一个 Future 完成时该怎么做。

和其他语言一样,回调地狱 在这种场景下也存在。

而 Scala 的 mapflatMap 可以在一定程度上缓解这个问题。

举个简单的例子:

1
2
3
4
5
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._

scala> def addTwo(n:Int):Future[Int] = Future { n + 2 }
addTwo: (n: Int)scala.concurrent.Future[Int]

当有另一个方法需要使用上述的 addTwo 方法返回的 Future 中的值时,可以使用 map 作为链式机制:

1
2
3
4
5
scala> def addTwoAndDouble(n:Int):Future[Int] =
addTwo(n).map { x:Int => x*2 }
addTwoAndDouble: (n: Int)scala.concurrent.Future[Int]
scala> addTwoAndDouble(4).onComplete(x => println(x))
Success(12)

当有嵌套 Future 情况出现时,可以使用 flatMap 消除嵌套:

1
2
3
4
5
6
scala> def addTwoAddTwo(n:Int):Future[Int] =
addTwo(n).flatMap { n2 => addTwo(n2) }
addTwoAddTwo: (n: Int)scala.concurrent.Future[Int]

scala> addTwoAddTwo(2).onComplete(x => println(x))
Success(6)

简单来说:

  • 当面对单个 Future 值时,使用 map 方法;
  • 当面对嵌套 Future 值时,使用 flatMap 方法来消除嵌套。