一、可空性:跨语言的“变量能不能空”的误会

1.1 你传的null,在Kotlin里可能是“违规操作”

我们先举个日常例子:你给熟悉的朋友发消息,说“有空就来玩”,朋友直接回“没空”你不会意外;但如果给AI助手发同样的消息,得专门说“你可以选空回复”,不然AI会报语法错误。Kotlin和Java的变量就像这俩“助手”,Java里除了基本类型,所有变量都能塞null,编译器不会拦;但Kotlin里,要是你说“这个变量必须有值”,它就会死死守住规则,不许塞null。 这段Kotlin示例明确标注了技术栈:

// 技术栈:Kotlin
class UserProfile {
    // 定义必须有用户名的方法,参数后没问号,代表非空
    fun showProfile(username: String) {
        println("当前登录用户:$username")
    }

    // 允许名字为空的方法,参数后带问号,代表可空
    fun showProfileNullable(username: String?) {
        // 空合并操作符:如果username是null,就显示匿名用户
        println("当前登录用户:${username ?: "匿名用户"}")
    }
}

现在用Java调用new UserProfile().showProfile(null),Java编译器根本不会报错,直接把null传过去;但到Kotlin运行时,会直接抛出NullPointerException,因为你违反了它的“非空规则”。反过来用Kotlin调用Java的方法时,要是Java方法没标记“参数可能为null”,Kotlin会把它当成“不确定能否空”的类型,给你弹警告,得手动处理null风险。

1.2 可空性的坑:谁来兜底空指针

这个场景在Android混合开发里特别常见,旧项目用Java写的,新功能用Kotlin,调用旧Java方法时,要是参数没加@NotNull注解,Kotlin会把它当成“不确定类型”,你要是不小心传了null,就会崩。从优缺点来看,Kotlin的可空设计确实减少了空指针,但和Java互操作时,相当于暂时拆了安全屏障,得自己当“守门员”;注意事项就是,只要Kotlin调用Java的方法,一定要看参数有没有@NotNull@Nullable注解,没加的话,要么用问号标记变量,要么加非空判断,避免崩溃。

二、重载冲突:同名方法的“认亲”难题

2.1 参数类型差一点,Java就会认错方法

再举个生活例子:你家里有两个遥控器,一个控电视一个控空调,都有“开”键,你喊“开”的时候,得看按的是哪个遥控器,不然就会开错设备。Java和Kotlin里的同名方法就像这俩遥控器,参数类型不一样,调用时得找对,不然就会冲突报错。 这段Kotlin示例技术栈明确:

// 技术栈:Kotlin
class NumberUtils {
    // 接收两个非空Int,计算和
    fun sum(a: Int, b: Int): Int {
        return a + b
    }

    // 接收两个可空Int,计算和时空值算0
    fun sum(a: Int?, b: Int?): Int {
        return (a ?: 0) + (b ?: 0)
    }
}

用Java调用new NumberUtils().sum(1, 2),Java能准确找到第一个方法;但要是调用new NumberUtils().sum(1, null),Java就懵了——因为Java没有可空类型,不知道要匹配哪个同名方法,会报“引用不明确”的错误。应用场景就是,把Kotlin写的工具类给Java项目用的时候,要是有同名的不同可空类型方法,就会触发这种冲突;优缺点是,Kotlin的重载更灵活,但Java不支持可空类型的重载,互操作时得留意参数差异;注意事项是,要是Kotlin的重载方法有可空和非空,最好用@JvmName注解改Java可见的方法名,避免冲突。

2.2 注解救场:给方法贴“专属标签”

刚才的冲突,用@JvmName注解就能解决,相当于给两个遥控器贴不同标签,你喊“开sum”和“开sumNullable”就不会混:

// 技术栈:Kotlin
class NumberUtils {
    fun sum(a: Int, b: Int): Int {
        return a + b
    }

    @JvmName("sumNullable") // 给Java展示的方法名改成sumNullable
    fun sum(a: Int?, b: Int?): Int {
        return (a ?: 0) + (b ?: 0)
    }
}

现在Java调用可空版本的和,直接用new NumberUtils().sumNullable(1, null)就不会报错了。

三、JVM注解:跨语言的“翻译官”

3.1 伴生对象:Java调用Kotlin静态方法的痛点

Kotlin没有静态方法,它的“静态功能”写在伴生对象里,比如:

// 技术栈:Kotlin
object AppConfig {
    val DEFAULT_PORT = 8080
    fun getTimeout(): Int {
        return 3000
    }
}

用Java调用这个getTimeout方法,得写AppConfig.INSTANCE.getTimeout(),因为伴生对象是单例,INSTANCE是它的实例名,调用起来像要搬凳子才够到杯子,很不顺手。解决办法是加@JvmStatic注解,把伴生对象的方法变成静态方法:

// 技术栈:Kotlin
object AppConfig {
    val DEFAULT_PORT = 8080
    @JvmStatic // 让Java把这个方法当成静态方法
    fun getTimeout(): Int {
        return 3000
    }
}

现在Java可以直接写AppConfig.getTimeout(),和调用Java自己的静态方法一样,不用管INSTANCE,这个注解就是把Kotlin的“专属静态”翻译成Java能看懂的“公共静态”。

3.2 可空性注解:给Java补“规则”

要是你写的Kotlin类是给Java用的SDK,得给参数加@NotNull注解,这样Java调用时,编译器会提前拦截null参数,就像你给朋友说“这个东西不能碰”,朋友就不会乱碰。比如:

// 技术栈:Kotlin
import org.jetbrains.annotations.NotNull

class UserService {
    fun login(@NotNull phone: String) {
        println("用手机号$phone登录")
    }
}

Java调用时,要是传了null,编译器会直接报错,不会等到运行时崩。应用场景就是对外SDK的开发,优缺点是JVM注解是跨语言的桥梁,能统一两边的规则,但纯Kotlin项目不用加,浪费性能;注意事项是,只有当你的类要给Java调用时,才加这些注解,别乱加。

文章总结

Kotlin和Java的互操作是混合开发的常态,就像和不同习惯的朋友相处,只要摸清楚对方的规则就能合作愉快。核心要注意三点:第一,处理可空性差异,Java传null到Kotlin非空方法时要格外小心;第二,重载同名方法时,用@JvmName避免冲突;第三,用@JvmStatic等注解简化Java调用,同时配合可空注解减少空指针风险。掌握这些,就能顺畅搭建混合开发项目,减少踩坑。