Kotlin空安全

news/2025/2/6 20:09:29/文章来源:https://www.cnblogs.com/ZJHqs/p/18701613

前言

访问空引用的成员变量就会导致空指针异常,在Java中被称作NullPointerException,简称NPE,Kotlin中NPE产生的原因只可能是以下几种:

  • 显式调用 throw NullPointerException()

  • 使用了!!操作符

  • 数据在初始化时不一致,例如:

    • 传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”)

    • 超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态

  • Java互操作

    • 企图访问平台类型的 null 引用的成员;

    • 用于 Java 互操作的泛型类型的可空性问题,例如一段 Java 代码可能会向 Kotlin 的 MutableList<String> 中加入 null,就需要一个 MutableList<String?> 才能处理。

    • 由外部 Java 代码引发的其他问题。

  1. 泄漏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.registervalue 初始化之前调用了 printValueLength,这导致使用未初始化的属性,从而引发空指针异常

  1. 超类构造函数调用一个开放成员

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类型,在我们尝试调用blength属性前,我们对b做了判空,再去使用。但这种方式要求b是不可变的情况(即在检测与使用之间没有修改过的局部变量 ,或是有幕后字段且不可覆盖的 val 成员),因为否则可能会发生在检测之后 b 又变为 null 的情况。一个典型的例子就是,我们在判断类成员变量不为空后,尝试获取类成员变量的属性依旧会提示空指针异常

安全调用

  1. 使用安全操作符?.

在一个可空的变量后面接?.操作符,即可安全地访问一个可空变量的属性,这在链式调用中很有用,比如下面我只是想判断一个集货任务的设备状态是否是处于Pending状态,如果按照上面的方式,将会写比较长的判断逻辑,但通过?.操作符,我们就可以这样实现:

DeviceStatus.PENDING_MERGE != mergeTaskInfo?.mergeTaskDeviceInfo?.currentDevice?.deviceMergeStatus

当在这一环中任意一个为null时,就会停止继续往下调用,直接返回null,显然不会与一个Int类型的值相等

  1. 使用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错误

补充

赋值

  1. 安全调用也可以出现在赋值的左侧。这样,如果调用链中的任何一个接收者为 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 时,才会对右侧表达式求值。

因为 throwreturn 在 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,后续使用需要使用?.

总结

一些实践经验

  1. 减少成员变量

尽可能少地减少在ActivityFragment中存储成员变量,如果实在需要一个成员变量用于记录上一次的值等信息,可以尝试将该成员变量移动到ViewModel中,然后由ViewModel提供一个get方法。其实这样修改表面上没有减少成员变量,但是如果将成员变量放到ViewModel中,结合LiveData,会显著减少成员变量的数量

  1. 尽量使用非空类型

尽量使用非空类型,只有在明确需要时才用可空类型。

在网络请求中,我们大多数情况下,需要后端返回的值非空才会做后续处理,因此我们可以在定义MultableLiveData时将类型设置为非空,确保视图监听获得的值一定非空,再去做后续处理,这样就可以减少判空处理

  1. 非必要不使用!!操作符

什么时候是必要呢?这里有一个典型的例子,在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

因此,对于这种理论上是可空的,而且应当被回收,但实际使用时我们可以确定它一定不为空,可以参考上面的方式来实现

  1. 使用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)}
}

先检查字段是否存在,并提供默认值

  1. 接口的入参根据是否必传选择可空或非空

对于必传的参数,将方法名中参数类型设置为非空,可以减少错误地请求发生。对于部分可以不传的参数,将其设置为可空,并为它默认赋值一个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,)

参考文档

  1. 空安全 · Kotlin 官方文档 中文版

  2. Kotlin刨根问底(一):你真的了解Kotlin中的空安全吗?空安全不是Kotlin特有的,其他很多编程语言也有,下面 - 掘金

  3. Kotlin的空安全真的安全吗?是一个常见的空安全异常,向空安全的变量赋了Null 值。但值变量是也是来自空安全变量,为 - 掘金

  4. 继承 · Kotlin 官方文档 中文版

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/879791.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kotlin控制流程

条件与循环 if表达式 Kotlin中的if与Java中的if大致上都差不多,但是Kotlin中没有三元运算符(A ? B : C),可以用if表达式作为代替,例如: Java int a = int a = System.currentTimeMillis() % 2 == 1L ? 1 : 0; Kotlin val a = if (System.currentTimeMillis() % 2 == 1L…

第一次用Markdown

标题 标题2 标题3 标题4 字体 字体 字体姓名 性别 年龄张三 男 20![das]() baidu

【测试基础】web3.0介绍

web3.0介绍 Web3.0也被称为下一代互联网,是对当前互联网(Web2.0)的演进和升级。其目标是实现一个更加去中心化、安全、用户拥有数据主权且具有更好互操作性的互联网环境。Web3.0的核心技术包括区块链、智能合约和加密货币等。 web2.0与web3.0区别 Web2.0和Web3.0的主要区别在…

区块链原理、技术与实践

区块链介绍 区块链是一种分布式账本技术,允许多个参与者共同维护一个不断增长的数据记录列表,每个区块包含一系列交易记录,并通过密码学方法与前一个区块链接起来,形成一个不可篡改和不可逆的链条。 这种基于共识的机制使得区块链具有高度的安全性和透明性。 区块链与传统W…

《高效能人士的七个习惯》

情感账户 勇气和体谅 大石头 自传式回应、同理心倾听:用你的话反映他们的感受和意思,而不是去评论、去判断是否正确

高效能人士的七个习惯

情感账户 勇气和体谅 大石头 自传式回应、同理心倾听:用你的话反映他们的感受和意思,而不是去评论、去判断是否正确

新春“码”启 | Cocos 3D 微信小游戏(第5天):分包构建和上传发布(完美收官)

新春开发 Cocos 3D 微信小游戏计划的第 5 天,详细介绍了如何利用Cocos Creator开发并发布一款3D微信小游戏,包括游戏状态机的设计理念,和微信小游戏主包大小限制时的解决方案——分包策略。从游戏设计、开发、调试到最后成功发布的全过程,为想要进入微信小游戏开发领域的开…

爬虫随笔(一)

爬虫随笔,某牛前几天一直在看js逆向,现在分享一下本人近期学习记录首先分享一个网站,这个网站可以获得request所需要的header和cookie https://curlconverter.com/ 爬取网站就不挂了简单观察发现,该网站是滑动加载,我们可以在滑动加载时获得我们所需要的接口,发现两个链…

Flow-CLI 全新升级,轻松对接 Sonar 实现代码扫描和红线卡点

Flow-CLI 使用的典型场景如:自定义开发一个 Sonar 扫描步骤,以在流水中触发 Sonar 扫描,并以扫描结果作为红线卡点,以保证代码质量;对接三方自有审批平台,在发布前进行检查审批,审批通过才允许发布。场景介绍 Flow-CLI 是云效流水线 Flow 推出的一款命令行工具,帮助用户…

皮克定理

小蓝鸟的面积S=B/2+I-1

两步构建 AI 总结助手,实现智能文档摘要

本次解决方案将向您介绍,如何通过函数计算 FC 阿里云百炼平台搭建智能 AI 总结助手,实现高效的文本自动总结和信息提取。在信息极度丰富的当下,如何从海量且复杂的文件资料中筛选出关键内容,成为了不少企业和个人急需解决的问题。本次解决方案将向您介绍,如何通过函数计算…

自定义桌面——获取天气

最近天气时不时会降温,经常用手机查看天气,感觉好不方便,今天决定把天气的功能添加到“自定义桌面”来,为了免费获取天气数据,竟然花了差不多一天的时间来制作完成。(假如一天人工费几百来算,这样真的值得吗?非常值得!为未来完善功能添砖加瓦打好基础!) 添加天气的功…