有人认为 null 是 Java 最糟糕的设计之一,这个极其有面向对象特色的特殊值实际生产中带来了极大的不便,因为它使得 NullPointerException 成为最常见的异常
为了搞定这个问题,Groovy 有安全运算符 (Safe Navigation Operator) ?. 来防止在对象是 null 是引发异常,而是直接返回 null ,Clojure 则将 null 弱化为了 nil ,使它可以在调用层级中向上冒泡,将问题转嫁到高层
Java 8 提供了 Optional<T> 类来解决这个问题,Scala 中的 Option[T] 则更加方便,因为 Java 原生的 Optional<T> 无论如何自己不可能是 null ,而 Scala 中的 Option[T] 在值不存在时就是对象 None,因此 Option 的使用是强制的,可以完全替代 None 来表示一个值的缺失
示例代码场景:
case class User ( id: Int, firstName: String, lastName: String, age: Int, gender: Option[String] ) object User { private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")), 2 -> User(2, "Johanna", "Doe", 30, None)) def findById(id: Int): Option[User] = users.get(id) def findAll: Iterable[User] = users.values }
创建
如果值并不缺失,Option[A] 就是一个 Some[A] ,所以可以直接实例化为 Some
val existed: Option[String] = Some("Hello world")
而在缺失情况下直接使用 None 对象就行了
val absent: Option[String] = None
或者以 null 为入参,这样写代码与 Java 库更通用
val absent: Option[String] = Option(null)
使用默认值
虽然有 isDefined 方法来检查值是否存在,但这样做太过笨重,大部分时候还是使用 getOrElse 提供默认值更方便
我们对 gender 直接访问
val user = User(2, "Johanna", "Doe", 30, None)
println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"
从这个样例类的构造也可以看出,Option 的使用使得可能出现 None 的位置非常清晰
链式使用
Option 的 orElse 方法接收另一个 Option 为参数,用于解决多个 Option 取第一个非 None 来进行使用的情况,也就是 getOrElse 的链式情况
典型的使用案例就是按优先级对多个不同的目录进行搜索,如下面例子,首先搜索 config 文件夹,然后调用 orElse 方法,以传递备用目录:
case class Resource(content: String)
val resourceFromConfigDir: Option[Resource] = None
val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
val resource = resourceFromConfigDir orElse resourceFromClasspath
模式匹配
由于 Some 是样例类,所以可以使用在模式匹配中:
user.gender.match {
case Some(gender) => println("Gender: " + gender)
case None => println("Gender: not specified")
}
在这里使用模式匹配并不方便,甚至非常多余,更常简的还是使用默认值的方法
同样,Option 也能使用 filter ,如下面这个例子,这就非常像一个集合了
User.findById(1).filter(_.age > 30) // None, because age is <= 30
User.findById(2).filter(_.age > 30) // Some(user), because age is > 30
User.findById(3).filter(_.age > 30) // None, because user is already None
Option 作为集合
我们知道 User.findById 得到的结果是一个 Option[User] ,这个 Option 对象完全可以当作 集合 (Collection) 使用,如使用 foreach, map, flatMap 进行操作
比方说 foreach,我们知道 Option 作为一个容器,包含元素个数不是 1 就是 0,所以 foreach 函数最多只会被调用一次,而如果是 None 则一次也不会被调用
User.findById(2).foreach(user => println(user.firstName))
Option 也可以用 map ,我们知道例子中 age 返回结果一定是一个 Int ,因此在 Option[User] 中访问年龄,得到的一定是一个 Option[Int] ,可能是 Some(Int) ,如果 Option[User] 不存在则结果还是 None
User.findById(1).map(_.age)
但如果我们访问 gender ,由于 gender 本身就是一个 Option 容器,直接用 Map 可能会造成嵌套,带来一个 Option[Option[String]] 作为结果
User.findById(1).map(_.gender)
这种情况下就可以使用 flatMap ,这和 List 中 flatMap 道理完全相同,两个嵌套的 Option 会被“拍扁”成为一个,由于 Option 本身只能含一个元素,多次嵌套也只能是一个,当然过程中只要 user 和 gender 一方为 None ,结果就是 None
val gender1 = User.findById(1).flatMap(_.gender) // gender is Some("male")
val gender2 = User.findById(2).flatMap(_.gender) // gender is None
val gender3 = User.findById(3).flatMap(_.gender) // gender is None
而如果我们操作一个包含 Option 的列表,效果也是类似的,只是 None 会直接被过滤,不出现在结果中
val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")
在 for 循环中使用
既然已经可以当作集合使用了,直接用 for 语句代替 flatMap 肯定也是可以的
for {
user <- User.findById(1)
gender <- user.gender
} yield gender // results in Some("male")
结合 for 循环与模式匹配使用
生成器左侧也是一个模式,通过在生成器左侧用 Some 模式也可以排除掉集合中 gender 为 None 的元素
for {
User(_, _, _, _, Some(gender)) <- UserRepository.findAll
} yield gender