Kotlin 朱涛 思维-4 空安全思维 平台类型 非空断言
目录
Kotlin 空安全思维
NPE 问题解决方案
Java 的解决方案
Java 中规避 NPE 有以下几种方案:
- 判空:这是
防御式编程
的一种体现,应用范围也很广泛 - 注解:借助
@Nullable
、@NotNull
之类的注解,通过 IDE 的提示帮我们规避 NPE - 封装数据:例如 1.8 中引入的
Optional
,这种手段的核心思路就是封装数据,不再直接使用 null
这三种方案都无法令人满意。
Kotlin 的解决方案
Kotlin 的类型系统与 Java 有很大的不同。在 Kotlin 当中,同样是字符串类型,却有三种表示方法。
String
:不可为空的字符串String?
:可能为空的字符串String!
:不知道是不是可能为空的字符串 -- 平台类型
Kotlin 通过让开发者必须明确规定每一个变量类型是否可能为空
,来帮我们规避 NPE 问题。
Kotlin 解决方案的局限性
在不与 Kotlin 以外的环境进行交互时,Kotlin 编译器已经可以帮我们彻底消灭 NPE 了。
但在与 Java 等其他语言环境打交道时,问题就变得复杂了。
- 从语言角度上看:Kotlin 不仅会和 Java 交互,还可以与其他语言交互,如果其他语言没有可空的类型系统,就一定要警惕起来
- 从环境角度上看:Kotlin 可以与其他外界环境交互,这些外界的环境中的数据,往往也是没有可空类型系统的,这时候也要警惕起来
Kotlin 中的平台类型
在 Kotlin 中访问 Java 代码时,把 Java 中的所有未知是否可为空的类型
,都看做是平台类型。平台类型用 !
来表示,比如String!
。
Java 代码
class TestJava {
@Nullable
public static String get1(@Nullable String s) {return s;}
@NotNull
public static String get2(@NotNull String s) {return s;}
public static String get3(String s) {return s;}
}
Kotlin 代码
get3() 方法没有任何 可空/非空 注解,它在 Kotlin 中会被认为是平台类型 String!
。
平台类型既能被当作可空类型,也能被当作不可空类型。
fun main() {
val s1: String? = TestJava.get1(null) // 可传 null
val s2: String = TestJava.get2("bqt")
val s3: String = TestJava.get2(null) // 编译报错,不可传 null
val s4: String? = TestJava.get3(null) // 既能被当作【可空类型】
val s5: String = TestJava.get3("bqt") // 也能被当作【不可空类型】
}
Kotlin 中的非空断言
- Kotlin 空安全调用语法:
?.
- Kotlin 空安全赋值语法:
?:
- Kotlin 非空安全的调用语法:
!!.
,这样的语法也叫做非空断言
如果使用非空断言
强行调用,可能会产生空指针异常。
非空断言的引入场景
非空断言代码主要在以下两种情况下会被引入:
- 使用 IDE 的
Convert Java File To Kotlin File
功能时,工具会自动生成带有非空断言
的代码 - 某些场景下,
Smart Cast
会失效,导致我们主动使用了非空断言
的代码
IDE 代码转换引入非空断言
public class JavaConvertExample {
private String name = null;
void test() {
if (name != null) {
System.out.println(name.length());
}
}
}
上面 Java 代码通过 IDE 转换成 Kotlin 代码后为:
class JavaConvertExample {
private var name: String? = null
fun test() {
if (name != null) {
println(name!!.length) // 非空断言
}
}
}
Smart Cast 失效的原因
Kotlin 支持 Smart Cast
,如果已经在 if 中判断了局部变量不等于空,就可以在 if 语句中转换成非空类型了,例如:
fun main() {
val tag: String? = TODO() // tag 是局部变量,使用 var 还是 val 修饰都可以
if (tag != null) {
println(tag.length) // 判断非空后,就可以被转换成【非空类型】了
}
}
下面的情况,Smart Cast 会失效:
var 声明的全局变量导致失效
private var name: String? = null // 将局部变量改为全局变量
fun main() {
if (name != null) {
println(name.length) // 编译报错
}
}
提示:Smart cast to 'String' is impossible 不可能发生的, because 'name' is a
mutable property
that could have been changed by this time
前面的 tag 是局部变量
,而下面的 name 是全局变量
。这就导致,即使 if 中 name 已经判空了,后一行代码运行时,name 也可能已经被改变了
(比如多线程场景)。所以此时没办法使用 Smart Cast
。
val 声明的可变变量导致失效
private val name: String? get() = null // 将可变变量 var 改为不可变变量 val
fun main() {
if (name != null) {
println(name.length) // 编译报错
}
}
提示:Smart cast to 'String' is impossible, because 'name' is a property that has open or custom getter
如何防止 Smart Cast 失效
借助 lateinit、懒加载,可以做到灵活初始化的同时,还能消灭可空类型。
① 改为函数传参的形式
fun test(name: String?) { // 改为函数参数
if (name != null) {
println(name.length) // 函数参数肯定支持 Smart Cast
}
}
函数的参数
是不可变的,因此,以函数参数的形式传进来后,就可以用于 Smart Cast
了。
② 改为使用 val
private val name: String? = null // 将可变变量 var 改为不可变变量 val
fun test() {
if (name != null) {
println(name.length) // val 肯定支持 Smart Cast
}
}
③ 借助局部变量
private var name: String? = null
fun test() {
var _name = name // 定义一个局部变量,使用 var 还是 val 修饰都可以
if (_name != null) {
println(_name.length) // 局部变量肯定支持 Smart Cast
}
}
④ 借助安全调用 ?.
private var name: String? = null
fun test() {
name?.let { println(it.length) } // 借助安全调用 ?.
}
这种方式和第三种方式,从本质上来讲是相似的,都是定义了一个局部变量。
⑤ 借助延迟初始化 lateinit
private lateinit var name: String // 【延迟初始化】【不可空类型】的变量
fun test() = if (::name.isInitialized) println(name.length) else println("未初始化")
fun main() {
test() //【未初始化】
name = "白乾涛"
test() //【3】
}
这种方案,其实就是将判空
问题变成了,判断是否已初始化
的问题。
⑥ 借助懒加载委托 by lazy
private val name: String by lazy { initName() } // 【不可变】的【非空】属性
private fun initName() = "Tom"
fun test() {
println(name.length)
}
借助这种方式,可以尽可能地延迟初始化,同时也消灭了可变性、可空性。
明确泛型的可空性
泛型 T 是可为空的类型
fun <T> f1(data: T) { // 注意,泛型 T 是可为空的类型
val set = sortedSetOf<T>() // 完全是 Java 中的 java.util.TreeSet
set.add(data) // TreeSet 内部无法存储 null,所以这一步会出发异常
}
fun main() {
f1("bqt") // 泛型实参自动推导为 String
f1(null) // 编译通过,运行时报 NullPointerException
}
上面代码中的泛型参数 T
,是可为空的类型,所以可以将 null
作为参数传进去的。然而,由于 TreeSet 内部无法存储 null
,所以在 set.add(data)
这里会抛出 NPE。
泛型 T 等价于 T:Any?
实际上,泛型 <T>
等价于 <T: Any?>
,因此,泛型的 T
可以接收 null
作为实参。
fun <T> f1(data: T) {} // 两者等价
fun <T : Any?> f2(data: T) {} // 两者等价
为泛型增加上界 Any
为了解决泛型的可空问题,可以为泛型 T
增加上界 Any
,这样当传入 null
时,编译器就会报错。
fun <T : Any> f1(data: T) {} // 增加泛型的边界限制【Any】
小结
Kotlin 的空安全思维,主要有四大准则:
- 警惕 Kotlin 与外界的交互
- 绝不使用非空断言
!!.
- 尽可能使用非空类型
- 明确泛型的可空性
2016-08-29
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/5817373.html