类型检查、转换与相等性

Kotlin 的类型检查和转换比 Java 更依赖编译器推断。你经常会看到 isas?、智能转换和 ==,它们看起来简单,但理解细节能避免很多迁移错误。

is!is

使用 is 判断运行时类型:

fun printLength(value: Any) {
    if (value is String) {
        println(value.length)
    }
}

if 分支中,value 被智能转换为 String,无需像 Java 那样再强制转换。

Java 对比:

void printLength(Object value) {
    if (value instanceof String text) {
        System.out.println(text.length());
    }
}

取反判断:

if (value !is String) return
println(value.length)

编译器知道 return 之后还能继续执行时,value 一定是 String

when 中的类型检查

fun describe(value: Any): String =
    when (value) {
        is String -> "字符串长度 ${value.length}"
        is Int -> "整数 ${value + 1}"
        is List<*> -> "列表大小 ${value.size}"
        else -> "未知"
    }

每个分支中都会根据类型自动智能转换。

智能转换的前提

智能转换只在编译器能证明变量不会在检查后被改变时成立。

通常可用:

  • 局部 val
  • 未被修改的局部 var
  • 某些同模块、不可覆盖、没有自定义 getter 的 val 属性。

通常不可用:

  • 可变属性 var
  • open 属性。
  • 有自定义 getter 的属性。
  • 被 Lambda 捕获并可能修改的变量。

示例:

class User(var name: String?)

fun printName(user: User) {
    if (user.name != null) {
        // println(user.name.length) // 可能无法智能转换
    }
}

因为 user.name 是可变属性,编译器不能保证它在检查后没有变化。常见写法是复制到局部变量:

val name = user.name
if (name != null) {
    println(name.length)
}

as:不安全转换

val value: Any = "Kotlin"
val text = value as String

如果类型不匹配,会抛出 ClassCastException

val number = value as Int // 运行时异常

只在你确定类型正确时使用 as

as?:安全转换

val value: Any = "Kotlin"
val number: Int? = value as? Int

转换失败返回 null,不会抛异常。配合 Elvis 运算符很常见:

fun stringLength(value: Any): Int {
    val text = value as? String ?: return -1
    return text.length
}

处理外部输入、反序列化结果、Java API 返回值时,优先考虑 as?

向上转型与向下转型

向上转型通常无需显式写:

interface Animal {
    fun speak()
}

class Dog : Animal {
    override fun speak() = println("woof")
    fun bark() = println("bark")
}

val animal: Animal = Dog()

向下转型需要显式操作:

val dog = animal as? Dog
dog?.bark()

如果你经常需要向下转型,可能说明抽象类型设计不足。优先考虑多态方法、密封类 when 或访问者模式。

结构相等:==

Kotlin 的 == 调用的是 equals(),并且安全处理 null

val a: String? = "hello"
val b: String? = "hello"

println(a == b) // true

a == b 大致等价于:

a?.equals(b) ?: (b === null)

这与 Java 很不同。Java 中 == 比较引用,equals() 比较结构;Kotlin 中 == 默认就是结构相等。

引用相等:===

=== 判断两个引用是否指向同一个对象:

val a = mutableListOf(1)
val b = a
val c = mutableListOf(1)

println(a === b) // true
println(a === c) // false
println(a == c)  // true

普通业务代码大多使用 ==。只有需要判断同一实例时才使用 ===

自定义 equals

普通类默认继承 Any.equals(),通常是引用相等。数据类会自动根据主构造函数属性生成结构相等。

普通类需要自定义结构相等时:

class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Point) return false
        return x == other.x && y == other.y
    }

    override fun hashCode(): Int =
        31 * x + y
}

重写 equals() 时必须同步重写 hashCode(),否则在 HashSetHashMap 中会出现错误行为。

数组相等

数组的 == 比较数组对象本身,不比较内容。比较内容要使用:

val a = intArrayOf(1, 2)
val b = intArrayOf(1, 2)

println(a == b)              // false
println(a.contentEquals(b))  // true

嵌套数组使用 contentDeepEquals()

浮点相等

当表达式静态类型是 FloatDouble 时,相等性遵循 IEEE 754。涉及 NaN-0.00.0 时要格外小心。

实践中不要用浮点数直接表达金额,也不要在精度敏感计算中直接用 == 判断结果是否相等。

实践建议

  • 类型不确定时优先 as?,少用 as
  • 智能转换失败时,先复制到局部 val,不要立刻写 !!
  • Kotlin 中 == 是结构相等,Java 迁移时尤其要注意。
  • 判断同一实例才用 ===
  • 自定义 equals() 必须同步自定义 hashCode()
  • 数组内容比较使用 contentEquals(),不是 ==

参考

  • 官方类型检查与转换:https://kotlinlang.org/docs/typecasts.html
  • 官方相等性:https://kotlinlang.org/docs/equality.html