Kotlin语言简洁高效,但在实际开发中,因使用习惯或忽视细节而导致的潜在问题并不少见,某些问题甚至可能引发线上严重故障。本文基于实战经验,总结了Kotlin编码中需要重点规避的几个“坑”,帮助你写出更健壮的代码。
一、应极力避免或谨慎使用的写法
1. 避免使用!!非空断言
使用!!操作符会绕过Kotlin的空安全机制,是空指针异常的潜在根源。即便当前上下文判断了非空,后续代码维护也可能疏忽。建议从设计上避免可空对象(如提供默认值),或使用更安全的方式,例如?.let作用域函数进行安全调用。
2. 避免使用as T?进行不安全的类型转换
as T?会在强制转换的对象并非T类型或其子类型时抛出ClassCastException异常。建议使用更安全的转换函数,如as?,它会在转换失败时返回null,避免程序崩溃。
3. 避免使用String.toXX()进行数值转换
这里特指String类的toInt()、toFloat()等直接转换方法。一个常见案例是:与后端接口约定某个字段为数字字符串,但若该字段意外传空,toInt()将抛出NumberFormatException。应优先使用Kotlin提供的安全转换函数,并妥善处理null值:
// 不安全
val num = str.toInt()
// 安全
val safeNum = str.toIntOrNull() ?: 0 // 提供默认值
4. 谨慎使用lateinit
在Android开发等场景中,lateinit有时是必要的(如在Activity.onCreate中初始化视图变量)。关键在于必须确保在首次访问属性前已完成初始化。滥用lateinit将导致“未初始化属性访问”异常。
二、与Java互操作时需注意的空安全
Kotlin与Java互操作时,其空安全机制可能被破坏,因为Java类型系统不具备可空性标识。假设有如下Java代码:
public interface ITest {
void test(int code, String msg); // msg 可能为null
}
public class TestCode {
public String getData() {
return data; // 可能返回null
}
}
在Kotlin侧编写代码时,应充分考虑Java可能传入null值:
class TestImpl : ITest {
// 将参数声明为可空类型,以安全处理Java调用
override fun test(code: Int, msg: String?) {
// 使用前进行空判断
}
}
// 以可空类型接收Java方法的返回值
val data: String? = TestCode().getData()
在处理来自Java的代码调用或返回值时,主动使用可空类型(?)是防御性编程的关键。
三、异常捕获与处理
Kotlin编译器不强制要求捕获已检查异常,这容易导致开发者遗漏对异常的处理,尤其是在I/O等操作中。
fun readFileToString(file: File): String {
val reader = FileReader(file) // 可能抛出IOException
// ... 读取操作
return content
}
上述代码编译正常,但未处理IOException,也未关闭资源,线上风险极高。对于可能抛出异常的操作,务必使用try-catch进行捕获,或明确将异常向上抛出。
特别注意:禁止在finally块中使用return语句,因为它会覆盖try或catch块中的返回值,导致逻辑错误。
四、集合操作与多线程安全
1. 警惕数组越界
直接通过索引访问集合元素前,务必判断集合大小,或使用Kotlin提供的安全访问函数:
val list = listOf("A", "B")
// 危险
println(list[2])
// 安全做法1:判断大小
if (list.size > 2) println(list[2])
// 安全做法2:使用安全函数
println(list.getOrNull(2))
2. 避免ConcurrentModificationException
当集合可能被多线程修改时,非原子化的“检查-再操作”极易引发ConcurrentModificationException。
val sharedList = mutableListOf<Int>()
// 线程不安全的写法
thread {
if (sharedList.size >= 10) {
sharedList.clear()
}
sharedList.add(1)
}
正确的做法是对共享资源的访问进行同步。这涉及到对并发控制机制的理解和应用:
// 使用 synchronized 进行同步
thread {
synchronized(sharedList) {
if (sharedList.size >= 10) {
sharedList.clear()
}
sharedList.add(1)
}
}
五、构造函数中禁止调用可被重写的方法
在父类构造函数(包括init初始化块)中调用可被子类重写的方法,可能导致访问到子类未初始化的属性。
open class Parent {
init {
println(getName()) // 危险:调用可被重写的方法
}
open fun getName() = "Parent"
}
class Child : Parent() {
private val childName = "Child"
override fun getName() = childName // 此时childName可能尚未初始化
}
fun main() {
Child() // 打印结果可能不是预期的“Child”,甚至是null
}
原因是父类构造函数的执行先于子类属性的初始化。因此,在设计类时,应避免在构造函数中调用非final的成员方法。