空安全的Kotlin,为何还会踏入NPE(空指针异常)的雷区?

222 阅读3分钟

Kotlin的空安全设计

在 Kotlin 中,空安全设计是一种类型系统的特性,用来避免空引用异常的发生。这个特性是 Kotlin 与 Java 之间重要的区别之一。

可空类型与非空类型

Kotlin 的类型系统区分可空类型和非空类型:

  • 非空类型:这是默认情况。如果声明一个变量的类型是非空的,那么这个变量在整个生命周期中不能持有 null 值。尝试将 null 赋值给这样的变量会导致编译错误。

  • 可空类型:通过在类型后添加 ? 来声明。例如,String? 表示可空字符串类型,这个变量可以持有一个字符串或者 null

处理空值

在 Kotlin 中,如果你试图直接访问可空类型的属性或方法,编译器会报错。为了安全地处理可空类型,Kotlin 提供了几种机制:

  • 安全调用操作符(?.):这允许你安全地调用可空对象的方法。如果对象不是 null,则执行调用;如果是 null,则不做任何操作并返回 null

  • Elvis 操作符(?:):这允许你为可能为 null 的表达式提供一个默认值。如果表达式不是 null,它就会返回原始值;否则,它会返回右侧的默认值。

  • 非空断言操作符(!!):这会强制告诉编译器一个值是非空的。如果值确实是 null,则会抛出空指针异常。这是一种危险的操作,因为它会在运行时引入可能的空指针异常。

  • let 函数:结合安全调用操作符使用,它允许你在非空值的上下文中执行代码块。

Kotlin中为什么还会出现空指针异常

虽然 Kotlin 设计了空安全特性,但在某些情况下仍然可能出现空指针异常:

显式调用

直接调用 throw NullPointerException()
比如:

fun causeNPEExplicitly() {
    throw NullPointerException("这是一个明确抛出的 NPE")
}

非空断言失败

使用 !! 操作符,而变量值为 null

fun causeNPEWithNonNullAssertion(value: String?) {
    // 如果 value 为 null,这里会抛出 NPE
    val nonNullValue: String = value!!
    println(nonNullValue)
}

// 使用示例
causeNPEWithNonNullAssertion(null) // 这将会抛出 NPE

Java 互操作性

Kotlin 与 Java 完全兼容,但 Java 没有内置的空安全特性。

当 Kotlin 代码调用 Java 代码时,由于 Java 并不区分可空类型和非空类型,Kotlin 无法保证从 Java 代码返回的值不为 null
如果 Kotlin 期望一个非空值,但 Java 返回了 null,这会导致 NPE。

比如,以下Java代码:

// JavaExample.java
public class JavaExample {
    public static String getNullString() {
        return null;
    }
}

在 Kotlin 中调用这个方法而不进行空检查会导致 NPE:

fun causeNPEFromJava() {
    // Java 方法返回 null,但 Kotlin 期望一个非空值
    val javaString: String = JavaExample.getNullString() // 这将会抛出 NPE
    println(javaString)
}

// 使用示例
causeNPEFromJava() // 这将会抛出 NPE

数据在初始化时不一致

当传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”)时,可能会出现NPE。 当超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态时,可能会出现NPE。

比如:

open class SuperClass {
    init {
        printSomething() // 在超类构造函数中调用了一个可被重写的成员函数
    }

    open fun printSomething() {
        println("Something from SuperClass")
    }
}

class SubClass : SuperClass() {
    private var message: String// 这个属性在使用前未初始化

    init {
        message = "Initialized in SubClass"
    }

    override fun printSomething() {
        println(message.length) // 使用未初始化的状态,将抛出NPE
    }
}

fun main() {
    SubClass() // 创建SubClass的实例,将导致NPE
}