1. 简介

Kotlin 提供了一系列 JVM 平台注解,用于增强 Kotlin 代码与 Java 的互操作性。

本文将深入探讨这些注解的实际用途、使用方式,以及它们如何影响 Kotlin 类在 Java 中的调用行为。对于混合使用 Kotlin 和 Java 的项目,掌握这些注解能有效避免“踩坑”。

2. Kotlin 的 JVM 注解概述

JVM 注解的核心作用是控制 Kotlin 代码编译为字节码的方式,以及生成的类在 Java 中的可见形式。

大多数 JVM 注解在纯 Kotlin 环境中无感,但 @JvmName@JvmDefault 在纯 Kotlin 中也会产生影响。

3. @JvmName

@JvmName 可用于文件、函数、属性、getter 和 setter,它定义了目标元素在字节码中的名称,也就是 Java 调用时所使用的名称。

⚠️ 注意:该注解不会改变 Kotlin 代码中的调用名。

3.1 文件名控制

默认情况下,Kotlin 文件中的顶层函数和属性会被编译到 文件名Kt.class 中。

例如,message.kt 文件:

package jvmannotation

fun getMyName(): String {
    return "myUserId"
}

class Message

编译后生成 MessageKt.classMessage.class。Java 调用方式为:

Message m = new Message();
String me = MessageKt.getMyName();

若想自定义辅助类名,可在文件首行添加 @file:JvmName

@file:JvmName("MessageHelper") 
package jvmannotation

fun getMyName(): String {
    return "myUserId"
}

此时 Java 调用变为:

String me = MessageHelper.getMyName(); // ✅ 使用新名称

⚠️ @file:JvmName 不影响普通类的文件名(Message.class 不变)。

3.2 函数名重命名

@JvmName 可修改函数在字节码中的名称。

@JvmName("getMyUsername")
fun getMyName(): String {
    return "myUserId"
}
  • Kotlin 调用:val username = getMyName()
  • Java 调用:String username = MessageHelper.getMyUsername();

此注解在以下两个场景非常实用:

3.3 解决函数名冲突

当函数名与自动生成的 getter/setter 冲突时,会编译报错:

val sender = "me" 
fun getSender(): String = "from:$sender" // ❌ 编译错误

错误信息:

Platform declaration clash: The following declarations have the same JVM signature (getSender()Ljava/lang/String;)

✅ 解决方案:使用 @JvmName 为函数指定不同的字节码名称:

@JvmName("getSenderName")
fun getSender(): String = "from:$sender"
  • Kotlin 调用:message.getSender()(函数)、message.sender(属性)
  • Java 调用:m.getSenderName()(函数)、m.getSender()(getter)

⚠️ 此类设计容易造成混淆,建议尽量避免。

3.4 解决类型擦除冲突

由于泛型擦除,JVM 认为以下两个方法签名相同,导致冲突:

fun setReceivers(receiverNames: List<String>) { }
fun setReceivers(receiverNames: List<Int>) { } // ❌ 编译错误

✅ 解决方案:为其中一个方法添加 @JvmName

@JvmName("setReceiverIds")
fun setReceivers(receiverNames: List<Int>) { }
  • Kotlin 调用:均可使用 setReceivers(...)
  • Java 调用:setReceivers(...)setReceiverIds(...)

3.5 自定义 Getter/Setter 名称

可为属性的 getter/setter 指定自定义名称:

class Message {
    @get:JvmName("getContent")
    @set:JvmName("setContent")
    var text = ""
}
  • Java 调用:m.setContent("..."), m.getContent()
  • Kotlin 调用:仍使用 message.text = "..."(不受影响)

❌ 错误示例:m.setContent("...") 在 Kotlin 中无法编译。

3.6 符合 Java 命名规范

Java Bean 规范中,boolean 属性的 getter 应以 is 开头。

var isEncrypted = true
var hasAttachment = true

Java 调用:

boolean encrypted = message.isEncrypted();     // ✅ 正确
boolean attachment = message.getHasAttachment(); // ❌ 不够优雅

✅ 改进:为 hasAttachment 添加 @get:JvmName

@get:JvmName("hasAttachment")
var hasAttachment = true

Java 调用变为:message.hasAttachment();(更符合习惯)

3.7 访问权限限制

使用 @JvmName 需注意访问修饰符:

  • ❌ 对 val 属性使用 @set:JvmName:编译报错,因为不可变属性没有 setter。
  • ⚠️ 对 private 成员使用 @get/set:JvmName:编译器会忽略注解并发出警告,因为私有成员不生成 getter/setter。

4. @JvmStatic 和 @JvmField

4.1 @JvmStatic

用于伴生对象(companion object)或单例对象(object)中的函数和属性,使其在 Java 中表现为静态方法/字段。

object MessageBroker {
    var totalMessagesSent = 0
    fun clearAllMessages() { }
}
  • Kotlin 调用:MessageBroker.totalMessagesSent
  • Java 调用:MessageBroker.INSTANCE.getTotalMessagesSent()(不够优雅)

✅ 添加 @JvmStatic

object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0
    @JvmStatic
    fun clearAllMessages() { }
}

Java 调用变为:MessageBroker.getTotalMessagesSent()(更自然)

4.2 @JvmField, @JvmStatic 与常量对比

object MessageBroker {
    @JvmStatic var totalMessagesSent = 0 // private static + getter/setter
    @JvmField var maxMessagePerSecond = 0 // public static field
    const val maxMessageLength = 0       // public static final field
}

对应的 Java 字节码等价于:

public final class MessageBroker {
    private static int totalMessagesSent;
    public static int maxMessagePerSecond;
    public static final int maxMessageLength = 0;
    public static final MessageBroker INSTANCE = new MessageBroker();

    // getter/setter for totalMessagesSent...
}

5. @JvmOverloads

Kotlin 支持函数参数默认值,但 Java 无法直接使用。

object MessageBroker {
    @JvmStatic
    fun findMessages(sender: String, type: String = "text", maxResults: Int = 10): List<Message> {
        return ArrayList()
    }
}
  • Kotlin 调用:可省略右侧参数 findMessages("me")
  • Java 调用:必须提供所有参数 findMessages("me", "text", 10)

✅ 添加 @JvmOverloads

@JvmStatic
@JvmOverloads
fun findMessages(sender: String, type: String = "text", maxResults: Int = 10): List<Message>

Kotlin 编译器会生成 n+1 个重载方法(n 为带默认值的参数个数),使 Java 也能享受便捷调用。

6. @JvmDefault

Kotlin 接口支持默认方法,但 Java 实现类可能无法继承默认实现。

6.1 Kotlin 默认方法与 Java 兼容性

interface Document {
    fun getType() = "document"
}

Java 实现类 HtmlDocument 会报错,提示未实现 getType() 方法。

✅ 解决方案:使用 @JvmDefault 注解,并配置编译器参数:

interface Document {
    @JvmDefault
    fun getType() = "document"
}

编译器需添加参数:

  • -Xjvm-default=enable:仅生成 JVM 默认方法
  • -Xjvm-default=compatibility:同时生成兼容性静态类

⚠️ 注意:从 Kotlin 1.5 起,@JvmDefault 已被弃用,推荐使用 -Xjvm-default=allall-compatibility 模式。

6.2 @JvmDefault 与接口委托

@JvmDefault 注解的方法在接口委托中会被排除。

interface Document {
    @JvmDefault fun getTypeDefault() = "document"
    fun getType() = "document"
}

class TextDocument : Document {
    override fun getType() = "text"
}

class XmlDocument(d: Document) : Document by d

测试结果:

val myTextDoc = TextDocument()
val xmlDoc = XmlDocument(myTextDoc)

assertEquals("text", myTextDoc.getType())
assertEquals("text", xmlDoc.getType())          // 委托给 myTextDoc
assertEquals("document", xmlDoc.getTypeDefault()) // 使用接口默认实现,未委托

7. @Throws

7.1 Kotlin 异常机制

Kotlin 无检查异常(checked exception),try-catch 非必需。

fun findMessages(sender: String): List<Message> {
    if (sender.isEmpty()) throw IllegalArgumentException()
    return ArrayList()
}

无论 Kotlin 还是 Java 调用,try-catch 都是可选的。

7.2 为 Java 生成检查异常

若希望 Java 调用时强制处理异常,使用 @Throws

@Throws(IllegalArgumentException::class)
fun findMessages(sender: String): List<Message> {
    if (sender.isEmpty()) throw IllegalArgumentException()
    return ArrayList()
}

此时 Java 调用若不捕获 IllegalArgumentException,将导致编译错误。

关键点@Throws 仅影响 Java 调用,Kotlin 侧仍无需 try-catch

8. @JvmWildcard 和 @JvmSuppressWildcards

8.1 泛型通配符基础

Java 需要通配符处理泛型协变:

List<? extends Number> list = new ArrayList<Integer>(); // ✅
// List<Number> list = new ArrayList<Integer>(); // ❌

Kotlin 语法更简洁:val list: List<Number> = ArrayList<Int>()

8.2 Kotlin 的通配符规则

Kotlin 编译器根据情况自动插入通配符:

  • 参数类型:非 final 类作为泛型参数时,会生成 ? extends T
    fun process(list: List<Number>) // -> Java: List<? extends Number>
    
  • 返回类型:默认不加通配符。
    fun getList(): List<Number> // -> Java: List<Number>
    

8.3 通配符手动控制

使用注解覆盖默认行为:

fun transformList(
    list: List<@JvmSuppressWildcards Number>
): List<@JvmWildcard Number>

Java 签名为:

List<? extends Number> transformList(List<Number> list)

⚠️ 注意:Java 官方指南不推荐在返回类型中使用通配符,但在特殊场景下这些注解很有用。

9. @JvmMultifileClass

解决多个文件顶层函数合并到同一 Java 类的问题。

两个文件都声明 @file:JvmName("MessageHelper") 会导致重复类名错误。

✅ 解决方案:在两个文件中均添加 @JvmMultifileClass

// MessageConverter.kt
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotation
fun convert(message: Message) = TODO()

// Message.kt
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotation
fun archiveMessage() = TODO()

Java 调用:

MessageHelper.archiveMessage();
MessageHelper.convert(new Message());

10. @JvmPackageName

该注解理论上可修改包名,但被标记为 internal,仅限 Kotlin 标准库内部使用,开发者无法调用。

11. 注解目标速查表

最权威的参考资料是 Kotlin 官方文档kotlin-stdlib.jar 中的源码。

以下是各注解适用目标的总结:

kotlin jmv anns

12. 总结

熟练掌握 Kotlin 的 JVM 注解是保障 Java/Kotlin 混合项目平滑协作的关键。它们虽小,但在实际开发中往往是避免互操作性问题的“利器”。完整示例代码已托管至 GitHub


原始标题:Guide to JVM Platform Annotations in Kotlin

« 上一篇: RxKotlin 介绍