前言java
Java 里的 Null Pointer Exception
写过一阵子的Java后, 应该会对NullPointerException (NPE)这种东西很熟悉,基本上会碰到这种异常,就是你有一个变量是 null,但你却调用了它的方法,或是取某个的值。
举例而言,下面的 Java 代码就会抛出NPE异常:数组
例1: String s1 = null; System.out.println("length:" + s1.length());
固然,通常来讲,咱们不多会写出这么明显的错误代码。
但另外一方,在 Java 的使用习惯说,咱们经常以「返回 null」这件事,来表明一个函数的返回值是否是有意义。函数
例2: //就是在 Java 里 HashMap 的 get() 方法,若是找不到对应的 key 值,就会反回 null: HashMap<String, String> myMap = new HashMap<String, String>(); myMap.put("key1", "value1"); String value1 = myMap.get("key1"); // 返回 "value1" String value2 = myMap.get("key2"); // 返回 null System.out.println(value1.length()); // 没问题,答案是 6 System.out.println(value2.length()); // 抛 NullPointerException
在上面的例子中,myMap 里没没有对应的key值,那么get()会传回null。
若是你像上面同样没有作检查,那极可能就会抛出 NullPointerException,因此咱们要像下面同样,先判断获得的是否是 null 才能够调用算字符串长度的方法。工具
例3: HashMap<String, String> myMap = new HashMap<String, String>(); myMap.put("key1", "value1"); String value1 = myMap.get("key1"); // 返回 "value1" String value2 = myMap.get("key2"); // 返回 null if (value1 != null) { System.out.println(value1.length()); // 没问题,答案是 6 } if (value2 != null) { System.out.println(value2.length()); // 没问题,若是 value2 是 null,不会被执行到 }
那咱们要怎么知道一个 Java 里某个函数会不会返回null 呢?
答案是你只能依靠 JavaDoc 上的说明、去查看那个函式的源码来看,再否则就是靠黑盒测试(若是你手上根本没有源码),又或者直接等他哪天爆掉再来处理。
Scala 里的 Option[T] 的概念
相较之下,若是你去翻 Scala 的 Map 这个类别,会发现他的回传值类型是个 Option[T],但这个有什么意义呢?学习
咱们仍是直接来看代码吧:测试
例4: // 虽然 Scala 能够不定义变量的类型,不过为了清楚些,我仍是 // 把他显示的定义上了 val myMap: Map[String, String] = Map("key1" -> "value") val value1: Option[String] = myMap.get("key1") val value2: Option[String] = myMap.get("key2") println(value1) // Some("value1") println(value2) // None
在上面的代码中,myMap 一个是一个 Key 的类型是 String,Value 的类型是 String 的 hash map,但不同的是他的 get() 返回的是一个叫 Option[String] 的类别。
但在各个Option 类别表明了什么意思呢?答案是他在告诉你:我极可能没办法回传一个有意义的东西给你喔!
像上面的例子里,因为 myMap 里并无 key2 这笔数据,get() 天然要想办法告诉你他找不到这笔数据,在 Java 里他只告诉你他会回传一个 String,而在 Scala 里他则是用 Option[String] 来告诉你:「我会想办法回传一个 String,但也可能没有 String 给你」。
至于这是怎么作到的呢?很简单,Option 有两个子类别,一个是 Some,一个是 None,当他回传 Some 的时候,表明这个函式成功地给了你一个 String,而你能够透过 get() 这个函式拿到那个 String,若是他返回的是 None,则表明没有字符串能够给你。
固然,在返回 None,也就是没有 String 给你的时候,若是你还硬要调用 get() 来取得 String 的话,Scala 同样是会报告一个 Exception 给你的。scala
至于怎么判断是 Some 仍是 None 呢?咱们能够用 isDefined 这个函式来判别,因此若是要和 Java 版的同样,打印 value 的字符串长度的话,能够这样写:code
例5: // 虽然 Scala 能够不定义变量的类型,不过为了清楚些,我仍是 // 把他显示的定义上了 val myMap: Map[String, String] = Map("key1" -> "value") val value1: Option[String] = myMap.get("key1") val value2: Option[String] = myMap.get("key2") if (value1.isDefined) { println("length:" + value1.get.length) } if (value2.isDefined) { println("length:" + value2.get.length) }
仍是改用 Pattern Matching 好了
我知道你要翻桌了,这和咱们直接来判断反回值是否是 null 还不是同样?!若是没检查到同样会出问题啊,并且这还要多作一个 get 的动做,反而更麻烦咧!
不过就像我以前说过的,Scala 比较像是工具箱,他给你各式的工具,让你本身选择适合的来用。
因此既然上面那个工具和本来的 Java 版本比起来没有太大的优点,那咱们就换下一个 Scala 提供给咱们的工具吧!
Scala 提供了 Pattern Matching,也就是相似 Java 的 switch-case 增强版,因此咱们上面的程序也能够改写成像下面这样:字符串
例6: // 虽然 Scala 能够不定义变量的类型,不过为了清楚些,我仍是 // 把他显示的定义上了 val myMap: Map[String, String] = Map("key1" -> "value") val value1: Option[String] = myMap.get("key1") val value2: Option[String] = myMap.get("key2") value1 match { case Some(content) => println("length:" + content.length) case None => // 啥都不作 } value2 match { case Some(content) => println("length:" + content.length) case None => // 啥都不作 }
上面是另外一个使用 Option 的方式,你用 Pattern Matching 来检查 value1 和 value2 是否是 Some,若是是的话就把 Some 里面的值抽成一个叫 content 的变量,而后再来看你要作啥。
在大多数的状况下,比起上面的方法,我会更喜欢这个作法,由于我以为 Pattern Matching 在视觉上比 if 来得更容易理解整个程序的流程。
但话说回来,其实这仍是在测试返回值是否是 None,因此充其量只能算是 if / else 的整齐版而已
Option[T] 是个容器,因此能够用 for 循环
以前有稍微提到,在 Scala 里 Option[T] 其实是一个容器,就像数组或是 List 同样,你能够把他当作是一个可能有零到一个元素的 List。
当你的 Option 里面有东西的时候,这个 List 的长度是一(也就是 Some),而当你的 Option 里没有东西的时候,他的长度是零(也就是 None)。
这就形成了一个颇有趣的现象--若是咱们把他当成通常的 List 来用,而且用一个 for 循环来走访这个 Option 的时候,若是 Option 是 None,那这个 for 循环里的程序代码天然不会执行,
因而咱们就达到了「不用检查 Option 是否为 None」这件事。
因而下面的程序代码能够就达成和咱们上面用 if 以及 Pattern Matching 的程序代码相同的效果:get
例7: // 虽然 Scala 能够不定义变量的类型,不过为了清楚些,我仍是 // 把他显示的定义上了 val myMap: Map[String, String] = Map("key1" -> "value") val value1: Option[String] = myMap.get("key1") val value2: Option[String] = myMap.get("key2") for (content <- value1) { println("length:" + content.length) } for (content <- value2) { println("length:" + content.length) }
咱们能够换个想法解决问题
话说上面的几个程序,咱们都是从「怎么作」的角度来看,一步步的告诉计算机,若是当下的状况符合某些条件,就去作某些事情。
但以前也说过,Scala 提供了不一样的工具来达成相同的功能,此次咱们就来换个角度来解决问题--咱们再也不问「怎么作」,而是问「咱们要什么」。
咱们要的结果很简单,就是在取出的 value 有东西的时候,印出「length: XX」这样的字样,而 XX 这个数字是从容器中的字符串算出来的。
在 Functional Programming 中有一个核心的概念之一是「转换」,因此大部份支持 Functional Programming 的程序语言,都支持一种叫 map()
的动做,这个动做是能够帮你把某个容器的内容,套上一些动做以后,变成另外一个新的容器。
举例而言,在 Scala 里面,若是有们有一个 List[String],咱们但愿把这个 List 里的字符串,全都加上" World" 这个字符串的话,能够像下面这样作:
例8: scala> val xs = List("Hello", "Goodbye", "Oh My") xs: List[String] = List(Hello, Goodbye, Oh My) scala> xs.map(_ + " World!") res0: List[String] = List(Hello World!, Goodbye World!, Oh My World!)
你能够看到,咱们能够用 map() 来替 List 内的每一个元素作转换,产生新的东西。
因此咱们如今能够开始思考,在咱们要达成的 length: XX 中,是怎么转换的:
先算出 Option 容器内字符串的长度
而后在长度前面加上 "length:" 字样
最后把容器走访一次,印出容器内的东西
有了上面的想法,咱们就能够写出像下面的程序:
例9: // 虽然 Scala 能够不定义变量的类型,不过为了清楚些,我仍是 // 把他显示的定义上了 val myMap: Map[String, String] = Map("key1" -> "value") val value1: Option[String] = myMap.get("key1") val value2: Option[String] = myMap.get("key2") // map 两次,一次算字数,一次加上讯息 value1.map(_.length).map("length:" + _).foreach(println _) // 把算字数和加讯息所有放在一块儿 value2.map("length:" + _.length).foreach(pritlnt _)
透过这样「转换」的方法,咱们同样能够达成想要的效果,并且一样不用去作「是否为 None」的判断。
再稍微强大一点的 for 循环组合
上面的都是只有单一一个 Option[T] 操做的场合,不过有的时候你会须要「当两个值都是有意义的时候才去作某些事情」的情况,这个时候 Scala 的 for 循环配上 Option[T] 就很是好用。
一样直接看程序代码:
例10: val option1: Option[String] = Some("AA") val option2: Option[String] = Some("BB"); for (value1 <- option1; value2 <- option2) { println("Value1:" + value1) println("Value2:" + value2) }
在上面的程序代码中,只有当 option1 和 option2 都有值的时候,才会印出来。若是其中有任何一个是 None,那 for 循环里的程序代码就不会被执行。
固然,这样的使用结构不仅限于两个 Option 的时候,若是你有更多个 Option 变量,也只要把他们放到 for 循环里去,就可让 for 循环只有在全部 Option 都有值的时候才能执行。
但我其实想要默认值耶……
有的时候,咱们会但愿当函数没办法返回正确的结果时,能够有个默认值来作事,而不是什么都不错。
就算是这样也没问题!
由于 Option[T] 除了 get() 以外,也提供了另外一个叫 getOrElse() 的函式,这个函式正如其名--若是 Option 里有东西就拿出来,否则就给个默认值。
举例来说,若是我用 Option[Int] 存两个无关紧要的整数,当 Option[Int] 里没东西的时候,我要当作 0 的话,那我能够这样写:
例11: val option1: Option[Int] = Some(123) val option2: Option[Int] = None val value1 = option1.getOrElse(0) // 这个 value1 = 123 val value2 = option2.getOrElse(0) // 这个 value2 = 0
因此 Option[T] 万无一失吗?
固然不是!因为 Scala 要和 Java 兼容,因此仍是让 null 这个东西继续存在,因此你同样能够产生 NullPointerException,并且若是你不注意,对一个空的 Option 作 get,Scala 同样会爆给你看。
例12: val option1: Option[Int] = null val option2: Option[Int] = None option1.foreach(println _) // 爆掉,由于你的 option1 原本就是 null 啊 option2.get() // 爆掉,对一个 None 作 get 是必定会炸的
我本身是以为 Option[T] 比较像是一种保险装置,并且这个保险须要一些时间来学习,也须要在有正确使用方式(例如在大部份的状况下,你都不该该用 Option.get() 这个东西),才会显出他的好处来。 只是当习惯了以后,就会发现 Option[T] 真的能够替你避掉不少错误,至少当你一看到某个 Scala API 的回传值的型态是 Option[T] 的时候,你会很清楚的知道本身要当心。