Kotlin 空安全最佳实践
前言
Kotlin
语言在设计时,重点关注了Java
的空指针异常(NullPointerException
,简称NPE),然而在使用时,由于NPE出现的原因及其多样化,对应的处理方式也有很多种,很多时候单靠一个variable != null
不能优雅地解决所有问题,在不同场合,我们可以通过不同的方式,来简化书写,明确语义。
Kotlin 产生 NPE 的原因
官网 文档中,列出了以下几种NPE产生原因:
显式调用
throw NullPointerException()
;使用了下文描述的
!!
操作符;!!
,非空断言运算符,作用是将任何值转换为非空类型,若该值为空,则会抛出NPE,例如:val b:String? = null val c = b!!.length println(c) /** 运行报错,输出: Exception in thread "main" kotlin.KotlinNullPointerException at preview.ChainCallKt.main(ChainCall.kt:25) **/
有些数据在初始化时不一致,例如当:
- 传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”);
class StudentUtils { ...... constructor() { add(this) } fun add(studentUtils: StudentUtils) { //do something } }
在构造方法中传入了 this ,但是 this 可能没有初始化完全(部分成员未初始化),可能造成 NPE
- 超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态;
class MyProgressBar(context: Context) : ProgressBar(context) { val bounds = Rect() override fun setProgress(progress: Int) { super.setProgress(progress) with(bounds) { val right = left + ((progress.toFloat() / max) * width) set(left, top, right.toInt(), bottom) } } } //在别处调用 val progress = MyProgressBar(this) // java.lang.NullPointerException: Attempt to read from field 'int android.graphics.Rect.left' on a null object reference
此处
bounds
的属性left
引用,回抛出 NPE,原因在于ProgressBar
的构造方法中调用了子类的setProgress
方法,而此时子类中bounds
尚未初始化Java 互操作:
- 企图访问平台类型的
null
引用的成员;
Java 中所有的类型都是可空的,在 Kotlin 中,Java 声明的类型会在编译期间映射成相应的 Kotlin 类型,比如
java.lang.String
对应 kotlin 中的kotlin.String!
,这时,可能由于在 Kotlin 中引用了可能为空的 javaString 类型,导致 NPE
val list = ArrayList<String>() // non-null (constructor result) list.add("Item") val size = list.size // non-null (primitive int) val item = list[0] // platform type inferred (ordinary Java object)
item
可能为空,因为list[0]
获取的是一个java
对象,是可空的。故而在使用诸如item.subString(1)
时,可能会抛出 NPE- 用于具有错误可空性的 Java 互操作的泛型类型,例如一段 Java 代码可能会向 Kotlin 的
MutableList<String>
中加入null
,这意味着应该使用MutableList<String?>
来处理它;
//StudentUtils.kt class StudentUtils { val mStudents:MutableList<MainActivity.Student> = mutableListOf() fun getStudentList():MutableList<MainActivity.Student> { return mStudents } fun add() { //编译报错!!! 因为列表是 MutableList,不允许添加 null 元素 mStudents.add(null) } } //SecondActivity.java
上面
StudentUtils
类里面定义了一个MutableList
的 mStudent ,所以在add
方法企图添加 null 到集合时,编译直接报错,但是在Java中却可以:public class SecondActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); StudentUtils utils = new StudentUtils(); //编译通过 utils.getStudentList().add(null); } }
- 由外部 Java 代码引发的其他问题。
public class SecondActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } public static MainActivity.Student getStudent() { return null; } }
在
SecondActivity
中的getStudent 方法返回了 null ,在 kotlin 类MainActivity
中引用,编译通过,但是回抛出异常:StudentUtils().mStudents.add(SecondActivity.getStudent()) //Caused by: java.lang.IllegalStateException: SecondActivity.getStudent() must not be null
- 企图访问平台类型的
空检查应用场景
只关注非空场景,不需要处理为空的case
使用
?
,声明可空类型,避免抛出NPE如果变量可能为空,则在定义时一定要使用
?
标示,当为空时,程序不会往下执行,也不会抛出NPE// 对于一些可能返回为空的方法,在赋值时一定要注意 fun getSudent(name: String?): Student? { if (!name.isNullOrEmpty()) return Student(name!!, 24) else return null } //如果不加 ? ,就会抛出NPE val student = getSudent("") print(student?.name) // 使用安全的类型转换 as? ,如果转换不成功则返回 null ,而不是抛出 ClassCastException val aInt: Int? = a as? Int //不会抛出异常,失败则返回 null val aInt: Int? = a as Int //可能抛出 ClassCastException // 使用可空的集合类型 val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull()
使用
!!
声明非空类型,为空则抛出NPEprint(student!!.name)
上述 1 中代码里,将 student 从
?
改为!!
修饰,则 student 为空时回抛出 NPE使用
let
标注代码块//student使用 ? 标注,则let里面一定不为空,为空则let内部所有代码都不会执行,它是空安全的 student?.let { print(it.name) print(it.age) } //不实用 ? 标注student,则 let 里面每次使用 student(it),都需要判断是否为空 student.let { print(it?.name) print(it?.age) }
非空和不非空,都需要处理
使用三元操作符
?:
val name:String = student?.name ?: "defaultName"
当为空时,输出的是
?:
后面的默认字符串fun foo(node: Node): String? { val parent = node.getParent() ?: return null val name = node.getName() ?: throw IllegalArgumentException("name expected") // …… }
使用
if else
Kotlin 中,if else 是表达式,是有返回值的,所以不仅能做空判断,还能作为返回值
//1. if else 作为表达式,返回String val name:String = if (student != null) { //student.name可能为空,所以用 !! 转成非空字符串 student.name!! } else { "defaultName" } //2. if else 作为条件判断 if (student != null) { print(student.name) } else{ print("other action") }
使用类型判断
is
来判断是否为空,如果符合is
条件,则 student 不为空val student = getSudent("") if (student is Student) { student.name }
也可以通过
when
来判断是否为空when (student) { null -> { print("null") } else -> print(student.name)//else 之后,student一定不为空,所以不需要用?修饰 } //对于类型不确定的,比如Any类型( 类似于Java object 类型),还可以结合when 和 is val student:Any = getSudent("") as Any when (student) { is Student -> { print(student.name) } is Teacher -> { print("is teacher") } }