Kotlin空安全
前言
访问空引用的成员变量就会导致空指针异常,在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: String
init {
// 假设在这里传递了 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 null
val 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 {
@Nullable
private String sequence;
@Nullable
public 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 {
@NotNull
private String sequence = "Hello World";
@NotNull
public 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 {
@NotNull
private final String sequence = "Hello World";
@NotNull
public final String getSequence() {
return this.sequence;
}
}
直接加了个final关键字在成员变量中,且没有实现set方法
延迟加载成员变量
class SequenceTest {
lateinit var sequence: String
}
public final class SequenceTest {
public String sequence;
@NotNull
public 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 method
public static void main(String[] var0) {
main();
}
}
Elvis的实现
fun test(str: String?) {
str?: return
println(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.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
因此,对于这种理论上是可空的,而且应当被回收,但实际使用时我们可以确定它一定不为空,可以参考上面的方式来实现
-
使用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.asJsonObject
val id = jsonObject.get("id").asInt
val name = if (jsonObject.has("user_name") && !jsonObject.get("user_name").isJsonNull) {
jsonObject.get("user_name").asString
} else {
"User"
}
val age = jsonObject.get("age").asInt
return 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,
)

浙公网安备 33010602011771号