您最近在代碼中遇到過NullPointerException(空指針異常)嗎? 如果沒有,那你一定是一個很細心的程序員。在Java應用程序中最常見的異常類型之一就是NullPointerException。只要該語言允許用戶將空值分配給一個對象,在某個時間點上對象為空將引發空指針異常,從而導致整個系統崩潰。
Java 8中引入了java.util.Optional
與Java相反,其他的開發語言,如Kotlin、Swift、Groovy等,能夠區分允許指向空值的變量和不允許指向空值的變量。換句話說,除非將變量顯式聲明為nullable(可空),否則它們不允許將空值分配給變量。在本文中,我們將概述不同編程語言中的可以減少或避免使用空值的一些特性。
Java Optionals
隨著在Java 1.8中引入的java.util.Optional
Null Checks
讓我們設計一個簡單的示例,其中有兩個類的用戶和地址,其中用戶中的必需字段只有用戶名,地址中的必需字段是street和number。任務是用給定的ID查找用戶的郵政編碼,如果沒有任何值,則返回一個空字符串。
假設還提供了UserRepository。實現這個任務的一種方法是:
上面的代碼,如果userRepository不是null,則此代碼不會拋出NullPointerException。但是,代碼中有三個if語句用於執行null檢查。檢查是否為空代碼的行數與為完成任務而編寫的代碼數量相當。
Optional Chaining
如果在不保證返回非空值的方法上使用Optionals作為返回類型,則上述實現也可以寫成:
第二個實現的代碼也第一個實現也好的很有限。空檢查只是被Optional.isPresent方法替換了。在調用每一個Optional.get()之前,都需要使用Optional.isPresent來判斷。在Java 10引入了一個更好的 Optional.orElseThrow ——它的使用方式一樣,但是方法名是警告說,如果值不存在,將拋出一個異常。
上面的代碼只是為了顯示 Optionals的醜陋用法。一種更優雅的方法是使可選API提供的一系列高階函數:
如果用戶存儲庫返回的Optional為空,則flatMap將只返回一個空可選項。否則,它將返回可選的包裝用戶的地址。這樣,就不需要進行任何空檢查。第二次flatMap調用也是如此。因此,Optional可被級聯,直到達到我們要查找的值。
Java 9增強功能
Optional API 在Java 9中進一步豐富,還有其他三個方法:or, stream 和ifPresentOrElse。
Optional.or 為連鎖選擇提供另一種可能性。例如,如果我們在內存中已經有一個用戶集合,我們想在進入存儲庫之前搜索這個集合,那麼我們可以做以下工作:
Optional.stream允許將可選的轉換為至多一個元素的流。假設我們要將userIds列表轉換為用戶列表。在Java 9中,可以這樣做:
Optional.ifPresentOrElse is similar to Optional.ifPresent from Java 1.8, but it performs a second action if the value is not present. For example, if the task was to print the ZIP code and it is provided or print a message otherwise, we could do the following:
Optional.ifPresentOrElse類似於Java 1.8中提供的Optional.ifPresent ,可是如果值不存在,則執行第二個操作。例如,如果任務是打印郵政編碼,如果提供了郵政編碼則打印,否則打印一條消息,代碼如下:
畢竟,Java最大的缺陷之一是它允許將每個非基本類型分配給null——甚至是Optional類型本身。如果findById方法只是返回null,那麼上面所描述的一切就變得毫無意義了。
Kotlin's 語言中Null類型安全
與Java不同的是,Kotlin語言的類型系統支持可空類型,這意味著除了數據類型的通常值外,還可以表示特殊值null的類型。默認情況下,所有變量都是不可空的。要聲明一個可空變量,聲明的類型後面應該有一個問號。
var user : User = null // 不能編譯,User是可空類型
var nullableUser : User? = null // 為 nullableUser對象聲明並分配一個Null值
val name = nullableUser.name // 不能編譯. 需要使用 '?.' 或 '!!'
var lastName = nullableUser?.lastName // 返回空,因為 nullableUser 是 null
val email = nullableUser!!email // compiles, but throws NullPointerException on runtime
lastName = nullableUser?.lastName ?: "" //返回空字符串
注意空安全調用之間的區別嗎?和非空斷言運算符!!正如名稱所示,如果反引用變量為null,則前者將立即返回null,而後者將拋出NullPointerException。你不想用!!除非你是nullpointerexception的愛好者。操作符類似於optionorelse。它返回在?:的左邊的表達式的值,如果它不是null。否則,它計算右邊的表達式並返回結果。.
Nullable Chaining
與Java中的Optionals 一樣,Kotlin中的可空值也可以通過使用例如null-safe調用操作符進行鏈接。在Kotlin中,findZipCode方法的實現將在一個語句中完成:
fun findZipCode(userId: String) =
userRepository.findById(userId)?.address?.zipCode ?: ""
Swift
Swift的運行與Kotlin非常相似。類型必須顯式地標記才能存儲nil值。這可以通過添加?後綴運算符用於字段或變量聲明的類型。不過,這只是在Swift標準庫中定義的Optional
var zipCode = nil // won’t compile, because zipCode is not optional
var zipCode : String = nil // same here
var zipCode : String? = nil // compiles, zipCode contains "none"
var zipCode : Optional
var zipCode : String? = "1010" // zipCode contains "some" String
Implicitly Unwrapped Optionals
Optionals can also be declared as implicitly unwrapped Optional by using the ! postfix operator on the type of the variable declaration. The main difference is that these can be accessed directly without the ? or ! operators. The usage of implicitly unwrapped Optionals is highly discouraged, except in very specific situations, where they are necessary and where you can be certain, that a value exists. There are very few cases in which this mechanism is really needed, one of which is the Interface Builder Outlets for iOS or macOS.
Here is an example of how it should NOT be done:
Optionals 也可以通過使用!變量聲明類型的後綴操作符。主要的區別是這些可以直接訪問而不需要?或!操作符。強烈建議不要使用隱式展開選項,除非是在非常特定的情況下,它們是必需的,並且您可以確定值的存在。
這裡有一個不應該這樣做的例子:
// zipCode will be nil by default and is implicitly unwrapped
var zipCode : String!
/*
* if zipCode has a value, it will work fine but in this case
* it hasn’t and will therefore throw an error
*/
zipCode.append("0")
達到同樣結果的正確方法是:
var zipCode : String?
zipCode?.append("0") // this line will return nil but no error is thrown
Optional Chaining
Optional 鏈接可用於使用?後綴運算符。許多對選項的調用可以鏈接在一起,因此命名為可選鏈接。這樣的表達式總是返回一個可選項,如果鏈中任何可選項都不包含,則該表達式將包含結果對象或none。因此,必須再次檢查可選鏈的結果是否為nil。這可以通過使用可選綁定、nil-合併操作符或guard語句來避免。
/*
* Optional chaining for querying the zip code,
* where findBy, address and zipCode are Optionals
* themselves.
*/
func findZipCodeFor(userId: String) -> String? {
return userRepository.findBy(userId: userId)?.address?.zipCode
}
Optional Binding
“if let”語句提供了一種安全的方式來 unwrap Optionals。如果給定的可選項包含none,則跳過If塊。否則,將聲明一個本地常量,該常量僅在if塊中有效。這個常量可以有與可選項相同的名稱,這將導致在塊中不可見的實際可選性。除了多個展開語句外,還可以向if let語句添加布爾表達式。這些語句之間用逗號(,)分隔,它的行為類似於&&操作符。
func printZipCodeFor(user: String) {
let zipCode = userRepository.findBy(userId: user)?.address?.zipCode
if let zipCode = zipCode {
print(zipCode)
}
}
func findZipCodeFor(userId: String, inCountry country: String) -> String? {
if let address = userRepository.findBy(userId: userId)?.address,
let zipCode = address.zipCode,
address.country == inCountry {
return zipCode
}
return nil
}
Nil-Coalescing Operator
無合併運算符由??如果可選項不包含任何值,則其目的是提供一個默認值。它的行為與 Kotlin’s Elvis操作員相似(?:)
let userId = "1234"
print(findZipCodeFor(userId: userId) ?? "no zip code found for user \(userId)")
操作符還接受另一個可選值作為默認值。因此,可以將多個nil合併操作符鏈接在一起。
func findZipCodeOrCityFor(user: String) -> String {
return findZipCodeFor(userId: user)
?? findCityFor(userId: user)
?? "neither zip code nor city found for user \(user)"
}
Guard
保護語句,顧名思義,是在它後面保護代碼。在方法中,檢查方法參數的有效性通常是在最開始。但是,如果可選項不包含任何選項,它也可以打開選項(類似於可選綁定)並“保護”後面的代碼。一個保護語句只包含一個條件和/或一個未包裝的語句和一個強制的else塊。編譯器通過使用控制傳輸語句(返回、拋出、中斷、繼續)或調用從未返回類型的方法來確保這個else塊退出其封閉範圍。可選項的未包裝值可以在保護語句的封閉範圍中看到,在這裡可以像使用普通常量一樣使用它。保護語句使代碼更具可讀性,並防止大量嵌套if語句。
func update(user: String, withZipCode zipCode: String) {
guard let address = userRepository.findBy(userId: user)?.address else {
print("no address found for \(user)")
return
}
address.zipCode = zipCode
}
結論
當請求的值沒有被信任時,建議使用Java Optionals作為API的返回類型。這樣,將鼓勵API的客戶端檢查返回值是否存在,並通過使用可選的API編寫更乾淨的代碼。然而,最大的缺陷之一是Java不能強制程序員不分配null值。其他現代語言,如Kotlin和Swift,被設計成能夠區分允許表示空值的類型和不允許表示空值的類型。此外,它們提供了一組豐富的特性來處理可空變量,從而最小化空引用異常的風險。
閱讀更多 程序你好 的文章