前言
访问空引用的成员变量就会导致空指针异常,在Java中被称作NullPointerException
,简称NPE
,Kotlin中NPE
产生的原因只可能是以下几种:
-
显式调用
throw NullPointerException()
-
使用了
!!
操作符 -
数据在初始化时不一致,例如:
-
传递一个在构造函数中出现的未初始化的
this
并用于其他地方(“泄漏this
”) -
超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态
-
-
Java互操作
-
企图访问平台类型的
null
引用的成员; -
用于 Java 互操作的泛型类型的可空性问题,例如一段 Java 代码可能会向 Kotlin 的
MutableList<String>
中加入null
,就需要一个MutableList<String?>
才能处理。 -
由外部 Java 代码引发的其他问题。
-
-
泄漏this
当你在构造函数中传递 this
引用到其他对象时,如果这些对象试图使用 this
,而此时对象尚未完全初始化,就可能导致空指针异常。
class MyClass {private val value: Stringinit {// 假设在这里传递了 this 并且立即使用了某个未初始化的属性SomeOtherClass.register(this)value = "Initialized"}fun printValueLength() {// 如果在 value 初始化之前被调用,会导致问题println(value.length)}
}object SomeOtherClass {fun register(myClass: MyClass) {// 模拟在注册时立即调用myClass.printValueLength()}
}fun main() {val myObject = MyClass()
}
在这个例子中,SomeOtherClass.register
在 value
初始化之前调用了 printValueLength
,这导致使用未初始化的属性,从而引发空指针异常
-
超类构造函数调用一个开放成员
open class Base {init {println("Base init block")// 调用开放成员println("Value from openMethod: ${openMethod()}")}open fun openMethod(): String {return "Base"}
}class Derived : Base() {private val value: String = "Derived"override fun openMethod(): String {// 使用未初始化的状态return value}
}fun main() {val derived = Derived()
}
在构造派生类的新实例的过程中,第一步完成其基类的初始化 (在之前只有对基类构造函数参数的求值),这意味着它发生在派生类的初始化逻辑运行之前。然后这里Base
的构造函数调用了 openMethod
,而在 Derived
中,value
尚未初始化,从而导致潜在的空指针异常。
声明可空/非空变量
可空变量
只需要在声明变量的时候在变量的类型后面加个?
操作符,即可将变量声明为可空,例如:
var canNullVar: String? = null // 合法
非空变量
在声明变量时不在变量的类型加?
操作符,或者直接给该变量赋值,声明的变量就是非空的,例如:
var notNullVar: String = "12345" // 合法
var notNullVar: String = null // 非法
// --------------------
var notNullVar = "12345" // 合法
notNullVar = null // 非法
如何访问可空变量的属性
在条件中检测null
与Java一样,可以在用一个可空的变量前,先对该变量进行判空,再继续使用,例如:
fun main() {val b: String? = "Kotlin"if (b != null && b.length > 0) {print("String of length ${b.length}")} else {print("Empty string")}
}
这里b
是一个可空的String类型,在我们尝试调用b
的length
属性前,我们对b
做了判空,再去使用。但这种方式要求b
是不可变的情况(即在检测与使用之间没有修改过的局部变量 ,或是有幕后字段且不可覆盖的 val
成员),因为否则可能会发生在检测之后 b
又变为 null
的情况。一个典型的例子就是,我们在判断类成员变量不为空后,尝试获取类成员变量的属性依旧会提示空指针异常
安全调用
- 使用安全操作符
?.
在一个可空的变量后面接?.
操作符,即可安全地访问一个可空变量的属性,这在链式调用中很有用,比如下面我只是想判断一个集货任务的设备状态是否是处于Pending状态,如果按照上面的方式,将会写比较长的判断逻辑,但通过?.
操作符,我们就可以这样实现:
DeviceStatus.PENDING_MERGE != mergeTaskInfo?.mergeTaskDeviceInfo?.currentDevice?.deviceMergeStatus
当在这一环中任意一个为null时,就会停止继续往下调用,直接返回null,显然不会与一个Int
类型的值相等
- 使用
let
配合安全操作符
fun main() {val listWithNulls: List<String?> = listOf("Kotlin", null)for (item in listWithNulls) {item?.let { println(it) } // 输出 Kotlin 并忽略 null}
}
使用let
只会对非空值做某个操作
!!
操作符
非空断言运算符!!
将任何值转换为非空类型,若该值为 null
则抛出异常。例如
var canNullVar: String? = null
str!!.length
此时会报Exception in thread "main" java.lang.NullPointerException
错误
补充
赋值
- 安全调用也可以出现在赋值的左侧。这样,如果调用链中的任何一个接收者为
null
都会跳过赋值,而右侧的表达式根本不会求值,例如:
// 如果 `person` 或者 `person.department` 其中之一为空,都不会调用该函数:
person?.department?.head = managersPool.getManager()
Elvis
操作符
当有一个可空的引用 b
时,可以说“如果 b
不是 null
,就使用它;否则使用某个非空的值”:
val l: Int = if (b != null) b.length else -1
除了写完整的 if
表达式,还可以使用 Elvis 操作符 ?:
来表达:
val l = b?.length ?: -1
如果 ?:
左侧表达式不是 null
,Elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为 null
时,才会对右侧表达式求值。
因为 throw
和 return
在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。这可能会很方便,例如,检测函数参数:
fun foo(node: Node): String? {val parent = node.getParent() ?: return nullval name = node.getName() ?: throw IllegalArgumentException("name expected")// ……
}
类型转换
如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException
。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null
:
val aInt: Int? = a as? Int
可空类型的集合
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull
来实现:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int= nullableList.filterNotNull()
Kotlin是如何实现空安全的
可空成员变量
class SequenceTest {var sequence: String? = null
}
将其转为字节码之后再生成Java代码,如下:
点击Tools -> Kotlin -> show Kotlin bytecode -> Decomplie
import kotlin.Metadata;
import org.jetbrains.annotations.Nullable;@Metadata(mv = {1, 9, 0},k = 1,d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0005\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002R\u001c\u0010\u0003\u001a\u0004\u0018\u00010\u0004X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0005\u0010\u0006\"\u0004\b\u0007\u0010\b¨\u0006\t"},d2 = {"LSequenceTest;", "", "()V", "sequence", "", "getSequence", "()Ljava/lang/String;", "setSequence", "(Ljava/lang/String;)V", "Study"}
)
public final class SequenceTest {@Nullableprivate String sequence;@Nullablepublic final String getSequence() {return this.sequence;}public final void setSequence(@Nullable String var1) {this.sequence = var1;}
}
可以看到,只是在成员变量上面加了个Nullable
注解
非空可变成员变量
class SequenceTest {var sequence: String = "Hello World"
}
转成Java实现后:
public final class SequenceTest {@NotNullprivate String sequence = "Hello World";@NotNullpublic final String getSequence() {return this.sequence;}public final void setSequence(@NotNull String var1) {Intrinsics.checkNotNullParameter(var1, "<set-?>");this.sequence = var1;}
}public static void checkNotNullParameter(Object value, String paramName) {if (value == null) {throwParameterIsNullNPE(paramName);}
}
添加了NotNull
注解,并在赋值时做了判空抛出空指针异常的处理
非空不可变成员变量
class SequenceTest {val sequence: String = "Hello World"
}
public final class SequenceTest {@NotNullprivate final String sequence = "Hello World";@NotNullpublic final String getSequence() {return this.sequence;}
}
直接加了个final关键字在成员变量中,且没有实现set方法
延迟加载成员变量
class SequenceTest {lateinit var sequence: String
}
public final class SequenceTest {public String sequence;@NotNullpublic final String getSequence() {String var10000 = this.sequence;if (var10000 == null) {Intrinsics.throwUninitializedPropertyAccessException("sequence");}return var10000;}public final void setSequence(@NotNull String var1) {Intrinsics.checkNotNullParameter(var1, "<set-?>");this.sequence = var1;}
}
public static void throwUninitializedPropertyAccessException(String propertyName) {throwUninitializedProperty("lateinit property " + propertyName + " has not been initialized");
}
可以看到相比于非空可变成员变量,它在get
方法的实现内还加了判空处理,也就是我们会看到的延迟初始化变量未被初始化的异常
参数可空方法
fun test(str: String?) {}
public final class SequenceTestKt {public static final void test(@Nullable String str) {}
}
同样只是在方法名内加了个Nullable
注解
参数非空方法
fun test(str: String) {}
public final class SequenceTestKt {public static final void test(@NotNull String str) {Intrinsics.checkNotNullParameter(str, "str");}
}
同样,除了在方法名加了个NotNull
注解外,还做了判空的处理
?.
的实现
fun test(str: String?) {println(str?.length)
}
public final class SequenceTestKt {public static final void test(@Nullable String str) {Integer var1 = str != null ? str.length() : null;System.out.println(var1);}
}
实际上就是帮我们做了判空,如果非空才做后续处理,让我们再加一层看看:
fun test(str: CharSequence?) {println(str?.toString()?.length)
}
public final class SequenceTestKt {public static final void test(@Nullable CharSequence str) {Integer var2;label12: {if (str != null) {String var10000 = str.toString();if (var10000 != null) {var2 = var10000.length();break label12;}}var2 = null;}Integer var1 = var2;System.out.println(var1);}
}
使用了局部变量用于赋值最后将这个局部变量赋值给结果
!!
的实现
fun test(str: String) {str!!.length
}
public final class SequenceTestKt {public static final void test(@NotNull String str) {Intrinsics.checkNotNullParameter(str, "str");str.length();}
}
也就是在实际调用前一下断言处理,如果真的非空就继续执行,如果为空直接抛出异常
let
的实现
Kotlin的源码在上面有,这里就不贴了,直接看是如何实现的,也就是先判空,非空才做后续操作
public final class SequenceTestKt {public static final void main() {List listWithNulls = CollectionsKt.listOf(new String[]{"Kotlin", null});Iterator var2 = listWithNulls.iterator();while(var2.hasNext()) {String item = (String)var2.next();if (item != null) {int var5 = false;System.out.println(item);}}}// $FF: synthetic methodpublic static void main(String[] var0) {main();}
}
Elvis
的实现
fun test(str: String?) {str?: returnprintln(str.length)
}
public final class SequenceTestKt {public static final void test(@Nullable String str) {if (str != null) {int var1 = str.length();System.out.println(var1);}}
}
as
的实现
fun test(str: Any?) {val result = str as String
}
public final class SequenceTestKt {public static final void test(@Nullable Object str) {if (str == null) {throw new NullPointerException("null cannot be cast to non-null type kotlin.String");} else {String result = (String)str;}}
}
直接判空,如果为空就抛出异常
as?
的实现
fun test(str: Any?) {val result = str as? String
}
public final class SequenceTestKt {public static final void test(@Nullable Object str) {Object var10000 = str;if (!(str instanceof String)) {var10000 = null;}String result = (String)var10000;}
}
先新建一个临时变量,将原值赋值给它,随后判断是否为特定类型,如果不是的话赋值为null,再做强转。因此经过as?
处理后的参数可能为null
,后续使用需要使用?.
总结
一些实践经验
-
减少成员变量
尽可能少地减少在Activity
和Fragment
中存储成员变量,如果实在需要一个成员变量用于记录上一次的值等信息,可以尝试将该成员变量移动到ViewModel
中,然后由ViewModel
提供一个get
方法。其实这样修改表面上没有减少成员变量,但是如果将成员变量放到ViewModel
中,结合LiveData
,会显著减少成员变量的数量
-
尽量使用非空类型
尽量使用非空类型,只有在明确需要时才用可空类型。
在网络请求中,我们大多数情况下,需要后端返回的值非空才会做后续处理,因此我们可以在定义MultableLiveData
时将类型设置为非空,确保视图监听获得的值一定非空,再去做后续处理,这样就可以减少判空处理
-
非必要不使用
!!
操作符
什么时候是必要呢?这里有一个典型的例子,在Fragment中,实际上有两套生命周期,我们一般会使用ViewBinding
来快速获取视图的控件,ViewBinding
的生命周期应该是跟Fragment的视图生命周期保持一致,那么它应该在Fragment中是可空的,但我们平时使用的时候,肯定不希望每次都加个?.
,Android官方的推荐写法是:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?
): View? {_binding = ResultProfileBinding.inflate(inflater, container, false)val view = binding.rootreturn view
}override fun onDestroyView() {super.onDestroyView()_binding = null
}
视图绑定 | Android Developers
因此,对于这种理论上是可空的,而且应当被回收,但实际使用时我们可以确定它一定不为空,可以参考上面的方式来实现
-
使用Gson序列化与反序列化,尽量使用可空
后端返回的字段,一般把它标记为可空会比较好。这样做虽然对我们来说变得更麻烦了,但是如果遇到后端异常返回的时候,至少不会引发crash,因为空安全不是完全的安全,下面举一个例子:
data class User(val id: Int,val name: String,val age: Int
)fun main() {val json = """{"id": "1234254665","name": null,"age": 20}""".trimIndent()val user = Gson().fromJson(json, User::class.java)println(user.name.length)
}
编译器是允许我们直接获取user.name.length
的,但是返回的json里面,name是null,这就引发了空指针问题。
即使我们这样写,同样会抛出空指针异常
data class User(val id: Int,val name: String = "User",val age: Int
)
这是因为创建对象的途径是通过sun.misc.Unsafe
进行创建的对象默认赋值的是一个空值null,不会执行我们的默认赋值操作。
4.1 手动检查与设置默认值
data class User(val id: Int,var name: String?,val age: Int
)
fun main() {val json = """{"id": "1234254665","name": null,"age": 20}""".trimIndent()val user = Gson().fromJson(json, User::class.java)if (user.name.isNullOrEmpty()) {user.name = "User Name"}println(user.name)
}
4.2 自定义反序列化器
fun main() {val json = """{"id": 1234254665,"name": null,"age": 20}""".trimIndent()val gsonBuilder = GsonBuilder()gsonBuilder.registerTypeAdapter(User::class.java, UserDeserializer())val gson = gsonBuilder.create()val user = gson.fromJson(json, User::class.java)println(user.name)
}class UserDeserializer : JsonDeserializer<User> {override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): User {val jsonObject = json.asJsonObjectval id = jsonObject.get("id").asIntval name = if (jsonObject.has("user_name") && !jsonObject.get("user_name").isJsonNull) {jsonObject.get("user_name").asString} else {"User"}val age = jsonObject.get("age").asIntreturn User(id, name, age)}
}
先检查字段是否存在,并提供默认值
-
接口的入参根据是否必传选择可空或非空
对于必传的参数,将方法名中参数类型设置为非空,可以减少错误地请求发生。对于部分可以不传的参数,将其设置为可空,并为它默认赋值一个null
,可以有效减少赋值,下面一个提交质检的Body,只有当质检结果为damage时需要给可空对象进行赋值,后端又不想我们把所有的参数都传给他,所以先定义一个data class如下
data class RoughQcBody(@SerializedName("task_id")val taskId: String,@SerializedName("sku_id")val skuId: String,@SerializedName("qc_qty")val qcQty: Int,@SerializedName("damage_qty")var damageQty: Int? = null,@SerializedName("sku_quality")val skuQuality: Int,@SerializedName("damage_type")var damageType: Int? = null,@SerializedName("new_tracking_id")var newTrackingId: String? = null,@SerializedName("damage_rate")var damageRate: Int? = null,@SerializedName("damage_limit")val damageLimit: Int,@SerializedName("qc_decision")var qcDecision: Int? = null,@SerializedName("reject_all_qty")var rejectAllQty: Int? = null,
)
那么在qc结果为good的时候,就可以这样写:
if (SystemEnum.SkuQualityType.GOOD == skuQuality) {val roughQcBody = RoughQcBody(taskId = taskId,skuId = skuId,qcQty = checkSkuInfo.remainingQcQty,skuQuality = skuQuality,damageLimit = checkSkuInfo.damageLimit,)
参考文档
-
空安全 · Kotlin 官方文档 中文版
-
Kotlin刨根问底(一):你真的了解Kotlin中的空安全吗?空安全不是Kotlin特有的,其他很多编程语言也有,下面 - 掘金
-
Kotlin的空安全真的安全吗?是一个常见的空安全异常,向空安全的变量赋了Null 值。但值变量是也是来自空安全变量,为 - 掘金
-
继承 · Kotlin 官方文档 中文版