Kotlin 的可空性

可空性

可空性是 kotlin 类型系统中帮助你避免空指针错误的特性,方案是把运行时的错误转变成编译期的错误。

可空类型

如果一个变量可以为 null ,那么对变量的方法的调用就是不安全的,可能会报空指针错误,如下函数:

public int strLen(String s) {
    // 当 s 为 null 时,会报空指针异常
    return s.length();
}

kotlin 中为了避免空指针,可以这样声明这个函数:

fun strLen(s: String) = s.length

如果在 kotlin 中调用这个函数时传入了 Null,编译器会报错,因为在 kotlin 中 ,String 只能表示字符串,而不能表示 null ,所以必须传入一个 String 实例,这是编译器强制要求的。如果你想让这个方法支持 null ,则需要在类型后面加上问号(?)来标记它:

fun strLen(s: String?) = if (s != null) s.length else 0    

? 可以加在任何类型的后面来表示这个类型的变量可以存储 null 引用: String?Int?MyCustomType? 等等。

注意: 如果你有一个可空类型的值,能对它进行的操作也会受到限制。

不能直接调用它的方法:

    fun strLen(s: String?) = s.length  

不能把它赋值给非空类型的变量:

    //Type mismatch.
    val y: String = x   

不能把可空类型的值传给拥有非空类型参数的函数:

    fun strLen(s: String) = s.length
    val x : String? = null
    //Type mismatch.
    strLen(x)  

小结:加了 ? 和不加 ? 可以看作是两种不同的类型,只有与 null 进行比较后,编译器才会智能转换成非空类型来处理,例如:

                       //与 null 进行比较  // 这里可以直接调用获取 length 了
fun strLen(s: String?) = if (s != null) s.length else 0

可空的和非空的对象在运行时没有什么区别:可空类型并不是非空类型的包装,所有的检查都发生在编译期,这意味着使用 Kotlin 的可空类型并不会在运行时带来额外的开销

安全调用运算符: “?.”

?. 运算符允许你把一次 null 检查和一次方法调用合并成一个操作。例如,表达式 s?.toUpperCase() 等同于 if (s != null) s.toUpperCase() else null

换句话说,如果你试图调用一个非空值的方法,这次方法调用会被正常地执行。但如果值是 null ,这次调用不会发生,而整个表达式的值为 null 。因此表达式 s?.toUpperCase() 的返回类型是 String?

安全调用不仅能调用方法,也能用来访问属性,例如:

class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? = employee.manager?.name

fun testManagerName() {
    val ceo = Employee("Fanda", null)
    val developer = Employee("liuhang", ceo)
    println(managerName(ceo))   // null
    println(managerName(developer))     // Fanda
}

如果你的对象中有多个可空类型的属性,可以在同一个表达式中使用多个安全调用:

class Address(val street: String, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    // 使用多个安全调用,如果有属性为空,则返回空,不会报空指针,非常方便
    val countryName = this.company?.address?.country
    return if (countryName != null) countryName else "Unkown"
}

Elvis 运算符: “?:”

Elvis 运算符接收两个运算数,如果第一个运算数不为 null ,运算结果就是第一个运算数,否则结果就是第二个运算数,例如:

// elvis 运算符
fun strLen(s: String?) = s?.length ?: 0

上述函数,如果 snull ,则返回 0 (第二个运算数),否则返回 length (第一个运算数)。

再看一个例子:

fun foo(s: String?) {
    // 这样写会报错,不能把可空的类型赋值给非空类型
    val t: String = s
    //这样可以,因为 null 被合并检验了,如果是 null ,会返回 ""
    val t: String = s ?: ""
}

可以把上面的多个安全调用方法再重写一下:

// 一行代码完成
fun Person.countryName() = company?.address?.country ?: "Unkown"

如果函数中需要检查先决条件,在值为 null 时立即返回一个值或者抛出异常,Elvis 运算符会非常方便,例如:

fun printPersonInfo(person: Person) {
    // 如果为空,直接抛异常
    val address = person.company?.address ?: throw IllegalArgumentException("No address")

    // 用 with 函数避免重复引用
    with(address) {
        println(street)
        println("$city , $country")
    }
}

fun testPersonInfo() {
    val address = Address("chaguang", "shenzhen", "china")
    val company = Company("dudu", address)

//    val person = Person("fanda",company)

    // Exception in thread "main" java.lang.IllegalArgumentException: No address
    val person = Person("fanda",null)

    printPersonInfo(person)
}

总结: ?: 就是合并了 null 操作,用其他的值(抛出异常,return 某个值,某个表达式等等)来代替 null 值。

安全转换: “as?”

我们之前说过,用 as 运算符可以代替 Java 中的 instanceof 来做类型转换,但是这种转换是不安全的,如果被转换的值不是你试图转换的类型,就会报错。当然可以结合 is 运算符来确保这个值的类型,但是调用不优雅。 as? 运算符尝试把值转换成指定的类型,如果值不是合适的类型,不会报错,会返回 null ,再结合 Elvis 运算符使用,非常方便:

不用 as? 时,是这样的:

override fun equals(other: Any?): Boolean {
    if (other == null || other !is Client) {
        return false
    }
    // 因为上面判断了类型,所以这里编译器会对 other 进行智能转换为 Client
    return name == other.name && postalCode == other.postalCode
}

用了 as? 时,是这样的:

override fun equals(other: Any?): Boolean {
    val c = other as? Client ?: return false
    return name == c.name && postalCode == c.postalCode
}

非空断言: “!!”

最简单直率的处理可空类型值的方式,可以把任何值转换成非空类型,但如果对 null 值做断言,会抛出异常。本质上是告诉编译器: 我认为这个值不为 null ,编译器不用做可空校验了,如果我错了,我准备好了接收这个异常。因此,非空断言,是可能会报异常的,但是如果你确信是不为 Null ,这时候你就可以使用非空断言来避免反复的检查。

fun ignoreNulls(s: String?) {
    // 如果 s 为 null ,这一行会报错
    val aNotNull: String = s!!  // s 做了非空断言,可以看作是非空类型的了
    println(aNotNull.length)
}

上述示例,如果 snull ,非空断言的那一行代码会报错。

注意:异常调用栈的跟踪信息只表明异常发生在哪一行代码,而不会表明异常发生在哪一个表达式,为了让跟踪信息更清晰精确地表示哪个值为 null ,最好避免在同一行中使用多个 !! 断言

// 最好不要写这样的代码
person.company!!.address!!.country

如果上面这一行代码发生了异常,是区分不了到底 company 的值为 Null ,还是 address 的值为 Null 的。

“let” 函数

如果你想要将一个可空值作为实参传递给一个只接收非空值的函数时,该怎么办?有两种办法 ,第一种是先做安全检查,检查后的值编译器才会认为合法,这时候就可以传入了,但是使用有点麻烦。第二种方法是使用标准库函数 let

fun sendEmail(email: String) {
    println("Sending email to $email")
}

fun testSendEmail() {
    val email: String? = "fanda.com"
//    sendEmail(email)    // 不能直接这样写
    if (email != null) sendEmail(email)     //先做校验,才能当作非空类型使用

    email?.let { sendEmail(it) }    // 通过安全调用 let 函数
}

let 函数做的所有事情就是把一个调用它的对象变成 lambda 表达式的参数 :

lambda 的参数就是 email
email?.let { email -> sendEmail(email) }) 

如果结合安全调用语法,它才能有效地把调用 let 函数的可空对象转换成非空类型,即上述 let 函数只在 email 的值非空时才被调用,所以你就能在 lambda 中把 email 当作非空的实参使用。如果 email 为空,那么什么都不会调用。

注意: 这里使用了 ?. 安全调用 let 函数,才能做非空的转换,而不是 let 函数本身具有非空转换的功能。如果不用安全调用,lambda 里面的实参还是可空的,即不能正常传递。

延迟初始化的属性

很多框架会在对象实例创建之后用专门的方法来初始化对象。例如 Android 中,Activity 的初始化就发生在 onCreate 方法中。 Kotlin 通常要求你在构造方法中初始化所有属性,如果某个属性是非空类型,你就必须提供非空的初始化值。否则,你就必须使用可空类型。如果你这样做,该属性的每次访问都需要 null 检查或者 !! 运算符。

class Activity{
    var view : View? =null

    fun onCreate() {
        view = View()
    }

    fun otherMethod() {
        // 需要这样调用,非常麻烦
        view?.onClickListener()
        view!!.onClickListener()
    }
}

为了解决这个麻烦,可以使用 lateinit 修饰符来把 view 声明成可以延迟初始化的非空类型。

class Activity {
    // 声明不需要初始化器的非空属性
    lateinit var view: View

    fun onCreate() {
        // 在该方法进行初始化
        view = View()
    }

    fun otherMethod() {
        // 不再需要 null 检查或断言
        view.onClickListener()
    }
}

注意:延迟初始化的属性都是 var ,因为需要在构造方法外修改它的值,而 val 属性会被编译成必须在构造方法中初始化的 final 字段。尽管 lateinit 修饰的属性是非空类型,但是你不再需要在构造方法中初始化它,但是如果你在属性被初始化之前就访问了它,会得到异常 lateinit property view has not been initialized

lateinit 属性常见的一种用法是依赖注入。在这种情况下,lateinit 属性的值是被依赖注入框架从外部设置的。为了保证和各种 Java 框架的兼容性,Kotlin 会自动生成一个和 lateinit 属性具有相同可见性的字段,如果属性的可见性是 public ,生成字段的可见性也是 public

public final class Activity {
   @NotNull
   public View view;    // 变成  public ,但是还是有 getter/setter 方法

   @NotNull
   public final View getView() {
      View var10000 = this.view;
      return var10000;
   }

   public final void setView(@NotNull View var1) {
      this.view = var1;
   }

   public final void onCreate() {
      this.view = new View();
   }

   public final void otherMethod() {
      View var10000 = this.view;
      var10000.onClickListener();
   }
}

可空性的扩展

为可空类型定义扩展函数是一种更强大的处理 null 值的方式。可以允许接收者为 null 的(扩展函数)调用,并在该函数中处理 null ,而不是在确保变量为 null 之后再调用它的方法。只有扩展函数才能做到这一点,普通成员函数的调用是通过对象实例来分发的,当实例为 null 时,方法不会被执行

标准库中的函数 isEmptyOrNullisNullOrBlank 就可以由 String? 类型的接收者调用,如下:

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {    // 该方法是 String? 的扩展方法,不需要安全调用
        println("Please fill in the required fields")
    }
}

无论 inputnull 还是字符串都不会导致任何异常。我们来看下 isNullOrBlank 函数的定义:

public inline fun CharSequence?.isNullOrBlank(): Boolean = 
    this == null || this.isBlank()

当你为一个可空类型(以 ? 结尾)定义扩展函数时,这意味着你可以对可空的值调用这个函数;并且函数体中的 this 可能为 null ,所以必须做显式检查。在 Java 中,this 永远是非空的,因为它引用的是当前你所在这个类的实例。而在 Kotlin 中 ,这并不永远成立,在可空类型的扩展函数中,this 可以为 Null

注意:当你定义自己的扩展函数时,需要考虑该扩展是否需要可空类型定义。默认情况下,应该把它定义成非空类型的扩展函数。如果发现大部分情况下需要在可空类型上使用这个函数,你可以稍后再安全地修改他(不会破坏其他代码)。

类型参数的可空性

Kotlin 中所有泛型和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。

fun <T> printHashCode(t: T) {    // T会被推导成 Any?
    // 因为 t 可能为 null ,所以需要安全调用
    println(t?.hashCode())
}

在该函数中,类型参数 T 推导出的类型是可空类型 Any? 。因此,尽管没有用问号结尾,实参 t 依然允许持有 null 。要使类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参。

// 给类型参数设置上界
fun <T : Any> printHashCode(t: T) {
    // 不需要安全调用
    println(t.hashCode())
}

注意: 必须使用问号结尾来标记类型为可空的,没有问号就是非空的,类型参数是这个规则唯一的例外。

可空性和Java

我们在 Kotlin 中通过可空性可以完美地处理 null 了,但是如果是与 Java 交叉的项目呢? Java 的类型系统是不支持可空性的,那么该如果处理呢? Java 中可空性信息通常是通过注解来表达的,当代码中出现这种信息时,Kotlin 就会识别它,转换成对应的 Kotlin 类型。例如: @Nullable String -> String?@NotNull String -> StringKotlin 可以识别多种不同风格的可空性注解,包括 JSR-305 标准的注解( javax.annotation 包下)、 Android 的注解( android.support.annitation ) 和 JetBrans 工具支持的注解( org.jetbrains.annotations )。

平台类型

如果没有上述所说的注解, Java 类型会变成 kotlin 中的平台类型,本质上就是 kotlin 不知道可空性信息的类型。即可以把它当作可空类型处理,也可以当作非空类型处理。你要像在 Java 中一样,对你在这个类型上做的操作负全部责任。如果你看作非空类型处理,但传入的是 null 值,就会报空指针异常。

比方说,我们在 Java 中定义了一个 Baby 类:

public class Baby {
    private final String name;

    public Baby(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

我们在 kotlin 中使用这个类时,可把它当作可空类型处理,也可以当作非空类型处理:

fun testBaby(baby: Baby) {
    //java.lang.IllegalStateException: baby.name must not be null
    println(baby.name.toUpperCase())    // 如果为Null,则抛异常
    println(baby.name?.toUpperCase())   // 安全调用,不会抛异常
}

注意:使用 Java API 时要特别小心,大部分的库都没有(可空性)注解,所以可以把所有类型都解释为非空,但那可能会导致错误。

平台类型表现为在类型后面加上 ! ,比如 String 的平台类型为 String! ,但是,kotlin 中,是不能声明一个平台类型的变量的,这些类型只能来自 Java 代码,可以用喜欢的方式来解释平台类型:

val name: String = baby.name    // 看作非空类型
val name2: String? = baby.name  // 看作可空类型

当然,如果平台类型是 Null ,看作非空类型时,赋值的时候会抛出异常。

继承 Java 类时的陷阱

当在 Kotlin 中重写 Java 的方法时,可以选择把参数和返回类型定义成可空的,也可以是非空的,例如:

// Java
public interface StringProcessor {
    void process(String value);
}

kotlin 中下面两种实现都可以接收:

class StringPrinter : StringProcessor {

    // 当作非空处理
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {

    // 当作可空处理
    override fun process(value: String?) {
        println(value ?: "")
    }
}

注意: 在实现 Java 类或者接口的方法时一定要搞清楚它的可空性。最好都定义成可空性的,因为方法的实现可以在 Java 中被调用,就算在 Kotlin 中定义成了非空的,但是在 Java 调用时是不起作用的,Java 没有可空的特性,如果在 Java 中传入了 null ,还是会报异常。

// Java 
public static void main(String[] args) {
    // 虽然在 kotlin 中声明成非空的,但是在 Java 中可以传入 Null
    new StringPrinter().process(null);    // 会报异常
}

总结:如果不能确保类型是非空的,最好都按可空类型处理,避免运行时报错。