其他分享
首页 > 其他分享> > Kotlin 中的 null safety

Kotlin 中的 null safety

作者:互联网

在编程语言中空引用(Null Reference)一直是一个不太好的概念。空引用带来了一系列的麻烦,在 2016 年的 QCon 中 Tony Hoare 博士将空引用称作十亿美元的错误

本次主要介绍一些常见的处理 null 的方式,特别是 kotlin 中的方案:

  1. null 带来的问题
  2. 常见的 null 处理方案
  3. kotlin 中的方案

从历史上看,在编程语言中空引用(Null Reference)一直是一个不太好的概念。空引用最早在 1964 年由Tony Hoare 博士发明,随后的主流语言中都延续了空引用的使用,包括 C、C++、 Java、C# 等。空引用在编程中带来了一系列的麻烦,在 2016 年的 QCon 中 Tony Hoare 博士将空引用称作十亿美元的错误(The Billion Dollar Mistake)

Null 带来的问题

破坏类型系统

静态检查的编程语言可以由编译器确保类型正确,不需要等到运行时实际执行代码。 例如在 Java 中当我们写下 x.toUpperCase(), 编译器会检查 x 的类型。如果 x 得类型是 String, 那么检查通过;如果是其他类型,比如 Servlet, 那么检查失败。 静态类型检查在编写大型的复杂软件时会提供强大的帮助。但是在 Java 中,由于任何引用都可以是 Null,编译器得静态检查变得非常痛苦。例如

代码变得繁琐

由于静态编译检查的失败,我们不得不做大量的运行时检查来防止 NullPointerException 得出现。例如

// 字符串判断if (str == null || str.equals("")) {}// 集合判断if (list == null || list.isEmpty()) {}

Google Guava 库中提供了 String.isNullOrEmpty 的方法来统一对 String 做检查。

使 API 得设计变得困难

由于 null 的存在,在设计 API 的时候很容易产生歧义,比如对于一个类似 getByName() 的方法,由于可能返回值可能为 null ,那么方法命名为 getByNameOrNullIfNotFound() 更合适。

以 Java Collection 中的 HashMap 为例,假设我们要从数据库中获取用户的电话号码,我们使用 HashMap 来缓存以避免重复请求。

Map<String, String> phoneNumberStore = new HashMap<>();phoneNumberStore.put("Li Lei", "138-1100-0011"); phoneNumberStore.get("Li Lei"); // 返回 Li Lei 的号码 "138-1100-0011"phoneNumberStore.get("Han Meimei"); // 返回 null,表示 Han Meimei 不存在

如果某个用户的号码不存在,我们仍然可以缓存,这样就不需要重新请求。

Map<String, String> phoneNumberStore = new HashMap<>();phoneNumberStore.put("Lucy", null); // Lucy 没有电话,我们缓存 null 代替phoneNumberStore.get("Lucy");

这里对于返回值就产生了歧义:

  1. 这个用户不在缓存中(Han Meimei)

  2. 这个用户在缓存中和,但是没有电话号码(Lucy)

语言设计变得困难

Java 可以自动的转换包装类型和原生类型,由于 null 的存在,使得这一行为变得诡异并且难以调试。

int x = null; // 编译错误// 编译通过,但是运行时抛出NullPointerExceptionInteger i = null;int x = i; // 运行时错误

如果将 Integer i = null 作为参数值传递到 int 类型的方法参数里,那更是一个灾难,甚至很难定位到 null 的对象。

一些常见的处理方式

鉴于 null 的种种问题,也诞生了一系列针对 null 处理的方案。

空对象模式

空对象模式(Null object pattern)是一种设计模式,核心是使用单独定义的空对象来代替 null 的返回值。空对象可以在数据不可用时提供默认的行为。 在空对象模式中,我们需要先创建一个指定各种要执行的操作的抽象类或接口、扩展该类的实体类,还创建一个未对该类做任何实现的空对象类,该空对象类将无缝地使用在需要检查空值的地方。

public interface Animal {    void makeSound() ;}public class Dog implements Animal {    public void makeSound() {        System.out.println("woof!");    }}public class NullAnimal implements Animal {    public void makeSound() {        // 静音的    }}

空对象模式可以比较好的简化调用端的处理逻辑,并且可以定制空对象的行为。但是空对象模式也有几个问题:

  1. 代码更加繁琐,需要定义一个抽象类或者接口,并且无法处理原生对象,比如 String, Integer

  2. 函数仍然可以返回 null

使用标签联合

标签联合(Tagged Union)是一种代数数据类型,也被称为 Sum Type, variants 等等。 对 Tagged Union 的抽象一般写为 Optional.T = Some(T) | None 。 在guava的早期版本中就提供了 com.google.common.base.Optional<T> 类来专门处理 null 相关的场景,Optional 类提供了三个静态方法来实例化:

  1. Optional.of(T) :构造一个 Optional 对象,其内部包含了一个非 null 的T数据类型实例

  2. Optional.absent() :构造一个代表空值的 Optional 对象

  3. Optional.fromNullable(T) :将一个T的实例转换为 Optional 对象,T可以为空

    同时 Optional 类也提供了几个实例方法来处理具体的值:

    1. boolean isPresent(): 判断当前包含的实例是否为 null

    2. T get(): 获取实例,如果为 null 则抛出 IllegalStateException

    3. T or(T): 获取实例,如果为 null 则以参数中的值代替

可以看到 Tagged Union 可以很好的处理 null,这一方法也被众多编程语言所采用,在 rust、Haskell、swfit 等语言中甚至完全去掉了 null 。从 Java 8 开始,Jdk 也内置了 java.util.Optional<T> 类来处理 null, 使用方法和 guava 类似。 但是由于 null 的存在,实际项目中我们还是很容易犯错,比如 Optional 类实例本身为 null,参数也可能为 null。

使用断言检查参数

guavaRxJava 等库中大量使用类似断言的方式检查输入参数, ObjectHelper.requireNonNull, Preconditions.checkNotNull 以及从 Java 7 开始加入的 java.util.Objects.requireNonNull 等,如果输入为 null 则立即抛出 IllegalArgumentException 。 这种方式可以在第一时间检查输入,防止 null 变量继续进入后续的逻辑,另外重新抛出异常的方式能帮助我们更好的定位到有问题的变量。但是这种方式还是在运行时做的检查,出现异常调用要等到运行时才能发现。

使用JSR 305 Annotation

JSR 305 定义了几个 Annotation 来标记变量或者方法返回值是否可以为 null。

  1. Nullness annotations: @Nonnull, @CheckForNull

  2. Nullable annotations: @Nullable

Android, Guava, JetBrains等都提供了类似的实现。使用Annotation标记之后,如果出现参数传递错误的情况,IDE中会给出相应的提示,一些静态检查工具如 FindBugs 等也会检查到问题,通过配置编译器能给出 Warning 。

比如在 Guava 中,使用 @Nullable 来标记可空参数(在 Guava 中所有参数默认都是不可以为 null 的)。

```javapublic static boolean isNullOrEmpty(@Nullable String string) {    return string == null || string.length() == 0; // string.isEmpty() in Java 6}```

Kotlin 的方案

Kotlin 中的类型

Kotlin 中对 null 的处理采用的是 UnTagged Union 的方案,和 Taggged Unio 的区别可以表示为 Optional.T = T | None 。这也是 Kotlin 和 Swift 的不同,语法上来说两者非常接近,但是从语义上又不完全相同。 Kotlin 中所有变量默认都是不可以为空的,变量是否允许为空,必须在定义时明确,例如:

var a: String = "abc"a = null // 编译错误var b: String? = "abc" // 变量声明时在类型的后面加上 ? 表示可空b = null // okprint(b)

Kotlin 可以保证调用 a 的方法不会出现 NullPointerException ,我们可以安全的调用 a 的任何实例方法:

val upper = a.toUpperCase()

但是,当我们要调用 b 的方法时,由于 b 是可空类型,编译器会报告错误:

val upper = b.toUpperCase() // error: variable 'b' can be null

那么我们怎么来处理 Kotlin 中的可空类型呢?

处理可空类型

Kotlin 没有针对可空类型进行隐式转换,但是提供了基于控制流的类型推断(Flow-sensitive typing),它可以很好的和 untagged union 结合。当编译器可以确定 Optional.T 类型的变量不为 null(None) 时,将自动转为 T 类型。 还是以上边的 b 为例:

if (b != null) {    val upper = b.toUpperCase() // 编译通过}

需要注意的是,只有在当前作用域内确定不会重现变化的属性才可以进行自动转换,比如线程不安全的可变变量是无法自动转换的,例如:

class SmartCast {    var nonSafe: String? = null    fun cannotCast() {        if (nonSafe != null) {            print(nonSafe.length) // 编译错误, nonSafe 无法进行自动转换        }    }}

但是,对于一些自定义的场景 Kotlin 无法帮我们确定一个可空变量是否为 null ,也就无法完成类型的自动转换,例如:

fun String?.isNotNull(): Boolean = this != nullfun foo(s: String?) {    if (s.isNotNull()) s.length // 编译错误,s 无法转换为 String}

从 Kotlin 1.3 版本开始,引入了实验性的 contract 特性,借助 contract 我们可以更好的处理自动类型转换,例如:

@kotlin.internal.InlineOnlypublic inline fun CharSequence?.isNullOrEmpty(): Boolean {    contract {        returns(false) implies (this@isNullOrEmpty != null)    }    return this == null || this.length == 0}fun bar(x: String?) {    if (!x.isNullOrEmpty()) {        println("length of '$x' is ${x.length}") // s已经自动转换为非空类型    }}

实例

fun main(args: Array<String>) {    var s: String? = "Hello world"    // print(s.length) ---- 编译错误    if (s != null) {        print(s.length)    }}

运行输出:

$ java kotlinc example.kt -include-runtime -d example.jar$ java java -jar example.jar 11

和 JSR 305 结合使用

Kotlin 和 Java 有非常好的互操作性,对于 JSR 305 也提供了很好的支持。在调用 Java 编写的 API 的时候,Koltin 默认认为所有的参数和返回值都是可空的,比如在继承类或者实现接口的时候,通过 IDE 生成的模板代码中,参数默认是都可空类型。

class CustomSerializer: org.codehaus.jackson.map.JsonSerializer<UserFollow>() {    override fun serialize(value: UserFollow?, jgen: JsonGenerator?, provider: SerializerProvider?) {        // TODO    }}

上面的例子中,我们实现一个自定义的 Jaskson JsonSerializer,IDE 默认生成的代码会将参数设定为可空类型。当然我们仍然可以手动将参数类型转换为非可空类型。

interface WithJSR305 {    @Nullable  String foo(String x);    @Nonnull    String bar(String x, @Nullable String y);    String baz(@Nonnull String x);}

我们定义个一个接口,给相应的参数和方法标注 JSR 305 的 annotation,IDE 会自动将对应的类型标注正确,更重要的是,如果类型不匹配,IDE 和 编译器都会报错。

class CustomImpl: WithJSR305 {    override fun foo(x: String?): String? {        TODO()    }    override fun bar(x: String?, y: String?): String {        TODO()    }    override fun baz(x: String): String? {        TODO()    }}

Reactor 库中使用 @NonNull , @Nullable , @NonNullApi 为 Kotlin 的 null safety 提供了全面的支持。

实例

首先定义个一个使用JSR305标记的 Java 类

import org.jetbrains.annotations.NotNull;import org.jetbrains.annotations.Nullable;public class JSR305Test {    @NotNull    String nonNullApi(@NotNull String x) {        return "NON_NULL: " + x;    }    @Nullable String nullableApi(@Nullable String x) {        if (x == null) {            return null;        }        return "nullable: " + x;    }}

在 kotlin 调用的时候如果使用了错误的类型,编译器会报错。

fun main(args: Array<String>) {    val jsr305 = JSR305Test()    println(jsr305.nonNullApi(null)) // 编译错误: 参数不可以是 null    println(jsr305.nullableApi("hello world").length) // 编译错误: 返回值可能为 null ,不能直接使用 length 属性}

使用相关操作符简化操作

很多时候我们需要可空类型的变量参数等等,但是每次使用之前都要进行空值判断比较繁琐,Kotlin 提供了一些操作符来帮忙我们简化操作。

使用 ?. 来进行安全调用

使用 ?. 可以安全的调用可空类型的方法和属性,如果为空那么返回null,否则调用对应的方法或者属性。

val b: String? = nullprintln(b?.toUpperCase())

这个例子中, b?.toUpperCase() 如果 b 不为 null,那么返回b.toUpperCase(), 否则返回 nullb?.toUpperCase 的型是 String?

对于嵌套比较深的复杂对象,使用 ?. 能够方便的进行链式调用。

user?.name?.firstName?.toUpperCase()

中间任意一个属性或者方法为 null 那么整个表达式就返回 null

实例
fun main(args: Array<String>) {    var userInput: String?    userInput = null    userInput?.let { // 只有当 userInput 不为 null 时执行 let 方法        println("$userInput is not null")    }}
$ java kotlinc example.kt -include-runtime -dexample.jar$ java java -jar example.jar$ 
三元操作符 ?:

对于一个可空变量,很多时候我们希望可以在不为空时直接使用对应的值,空时使用默认值, ?: 操作符可以实现对应的效果。

val l = b?.length ?: -1

?: 左边的部分不为 null 时 ?: 返回对应的值,否则返回右边的值另外 ?: 也遵循短路原则,如果左边的值不为 null 右边的表达式是会执行的。

在函数中, ?: 可以用来提前从函数中退出。

fun foo(node: Node): String? {    val parent = node.getParent() ?: return null    val name = node.getName() ?: throw IllegalArgumentException("name expected")    // ...}
实例
class Person(val name: String, val age: Int)fun main(args: Array<String>) {    val john : Person? = Person("John", 32)    val age = john?.age ?: 25    val offsetAge = age + 1 //编译通过    println("offsetAge: $offsetAge")    val ageWithThrow = john?.age ?: throw IllegalArgumentException("John is null")    val offsetThrowAge = ageWithThrow + 2 //编译通过    println("offsetThrowAge: $offsetThrowAge")}

运行结果:

$ java kotlinc example.kt -include-runtime -dexample.jar$ java java -jar example.jaroffsetAge: 33offsetThrowAge: 34
lateinit

通常 kotlin 类里的非空属性必须在构造期间初始化,但是很多时候我们会过其他方式来完成赋值操作,比如依赖注入(@Inject, @Autowired),单元测试的 setup 方法, @PostConstruct 等。这时如果我们属性声明为可空类型就会带来很多的麻烦,使用时都要做可空判断或者使用!! ,非常的不方便。

这种场景下,我们可以使用 lateinit 关键词来修饰属性,来避免null 相关检查。

class UserController() {    @Resource    private lateinit var userService: UserService    @GetMapping("/users/{uid}")    fun getUserFavPostList(@PathVariable("uid") uid: uid): User {        return userService.getUserInfo(uid) // 不需要空判断    }}

lateinit 使用有一定的条件限制,只能修饰 var 声明的属性,并且能有自定义的 getter/setter ,只能用在 class body 中的属性在 primary constructor 中的不可用。如果在属性初始化之前使用变量然会抛出 NPE ,从1.2开始,可以使用 isInitialized 来检查lateinit var 是否被初始化。

if (this::userService.isInitialized) {    println(userService.getUserInfo(uid))}

lateinit 的实现是基于 kotlin 的属性委托,使用 notNull 也以达到类似的效果。

实例

以 junit 的 testcase 为例:

import org.junit.Assertimport org.junit.Beforeimport org.junit.Testclass LateInitTest {    private lateinit var lateVar: String    @Before    fun setup() {        this.lateVar = "Hello world!"    }    @Test    fun call() {        Assert.assertEquals(12, lateVar.length)    }}

执行结果:

$ kotlinc example.kt -include-runtime -d example.jar-cp ./lib/junit-4.12.jar$ java -cp .lib/hamcrest-core-1.3.jar:./libjunit-4.12.jar:./example.jar org.junit.runner.JUnitCore LateInitTestJUnit version 4.12.Time: 0.008OK (1 test)
非安全操作符 !!

!! 会忽略变量的类型,强制转换为非空类型,但是这个操作符是不安的,如果变量为 null 那么会抛出 NPE

val upper = b!!.toUpperCase()

阅读全文: http://gitbook.cn/gitchat/activity/5d57af98b5ee3365573907eb

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

标签:String,val,Kotlin,类型,safety,可空,fun,null
来源: https://blog.csdn.net/valada/article/details/99699483