您最近在代码中遇到过NullPointerException(空指针异常)吗? 如果没有,那你一定是一个很细心的程序员。在Java应用程序中最常见的异常类型之一就是NullPointerException。只要该语言允许用户将空值分配给一个对象,在某个时间点上对象为空将引发空指针异常,从而导致整个系统崩溃。
Java 8中引入了java.util.Optional<T>类来处理这个问题。实际上,这些Optional's API 非常强大。有很多情况下,Optional's API 可以解决我们遇到的问题。然而,它们并不是仅仅为解决NullPointerException问题而设计的。此外,Optional本身很容易被误用。一个很好的指标就是经常发布的文章的数量,这些文章是关于Optionals应该或者不应该被使用的。
与Java相反,其他的开发语言,如Kotlin、Swift、Groovy等,能够区分允许指向空值的变量和不允许指向空值的变量。换句话说,除非将变量显式声明为nullable(可空),否则它们不允许将空值分配给变量。在本文中,我们将概述不同编程语言中的可以减少或避免使用空值的一些特性。
Java Optionals
随着在Java 1.8中引入的java.util.Optional<T>类,显著减少了空引用的情况。尽管如此,在创建或使用 Optional 时也需要注意一些问题。例如,如果值不存在, Optional.get()方法将抛出NoSuchElementException异常。如果提供的值为空,方法将抛出NullPointerException异常。因此,使用这两种方法都与直接使用空值对象有一样的风险。我们从 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的类型。默认情况下,所有变量都是不可空的。要声明一个可空变量,声明的类型后面应该有一个问号。
代码语言:javascript复制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方法的实现将在一个语句中完成:
代码语言:javascript复制fun findZipCode(userId: String) =
userRepository.findById(userId)?.address?.zipCode ?: ""
Swift
Swift的运行与Kotlin非常相似。类型必须显式地标记才能存储nil值。这可以通过添加?后缀运算符用于字段或变量声明的类型。不过,这只是在Swift标准库中定义的Optional<Wrapped>类型的一种简短形式。与普通类型不同,Swift选项不需要直接初始化或由构造函数初始化。它们默认为nil。Swift可选实际上是一个枚举,它有两种状态:none和some,其中none表示nil, some表示一个已wrapped的对象。
代码语言:javascript复制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<String> = nil // same as above
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 也可以通过使用!变量声明类型的后缀操作符。主要的区别是这些可以直接访问而不需要?或!操作符。强烈建议不要使用隐式展开选项,除非是在非常特定的情况下,它们是必需的,并且您可以确定值的存在。
这里有一个不应该这样做的例子:
代码语言:javascript复制// 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语句来避免。
代码语言:javascript复制/*
* 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语句添加布尔表达式。这些语句之间用逗号(,)分隔,它的行为类似于&&操作符。
代码语言:javascript复制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操作员相似(?:)
代码语言:javascript复制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语句。
代码语言:javascript复制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,被设计成能够区分允许表示空值的类型和不允许表示空值的类型。此外,它们提供了一组丰富的特性来处理可空变量,从而最小化空引用异常的风险。
请关注微信公众号:程序你好