前言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] 的時候,你會很清楚的知道本身要當心。