原文档 https://www.baeldung.com/kotlin/jvm-annotations
1、介绍
kotlin 提供了几个 annotations 用于 Kotlin 和 Java 更好的交互,在这个指南,我们将着重介绍 Kotlin 的 JVM annotations 的使用方法,以及他们的作用
2、Kotlin 中的 JVM Annotations
Kotlin 中的 JVM annotations 指导 kotlin的代码怎么样编译成二进制以及Java 代码中如何使用这些代码。
大部分 JVM annotations 在仅使用Kotlin是不需要的。但是@JvmName 和 @JvmDefault 在只用 Kotlin 的代码中也有相应的作用。
3、@JvmName
我们可以使用@JvmName annotation 作用于 files、functions、properties 、 getters 和 setters
在任何情况下,@JvmName 定义了编译成字节码后的名字,以及我们如何从 Java 代码中如何引用
这个annotation 不会改变我们通过koltin 调用这些 class 、function、getter 或者 setter 的名字
下面是使用详情
3.1、File 名称
默认的情况,所有在 Kotlin 文件的顶层 function 和 properties 会被编译成 filenameKt.class 所有的 classes 编译成 className.class
让我们创建一名字为 message.kt 的文件,在它的里面包含着 顶层的定义以及一个叫做 Message 的 class
package jvmannotation
fun getMyName(): String {
return "myUserId"
}
class Message {
}
编辑器会生产两个class 文件 MessageKt.class 和 Message.class
在 Kotlin 中调用:
val message = Message()
val msg = getMyName()
而在 Java 中调用:
Message message = new Message();
String msg = MessageKt.getMyName();
可以通过 把 @JvmName annotation 添加到文件的第一行,来修改MessageKt.class 名字
@file:JvmName("MessageHelper")
package jvmannotation
fun getMyName(): String {
return "myUserId"
}
class Message {
}
在 Kotlin 中调用不变
val message = Message()
val msg = getMyName()
在 Java 中修改为:
Message message = new Message();
String msg = MessageHelper.getMyName();
3.2、Function 名称
与 file 名称的作用类似,示例代码如下:
@file:JvmName("MessageHelper")
package jvmannotation
@JvmName("getMyUsername")
fun getMyName(): String {
return "myUserId"
}
class Message {
}
在 Kotlin 中调用:
val message = Message()
val msg = getMyName()
在 Java 中调用:
Message message = new Message();
String msg = MessageHelper.getMyUsername();
3.3、Function 名称分歧
如下代码:
val sender = "me"
fun getSender() : String = "from:$sender"
编译错误信息如下:
Platform declaration clash: The following declarations have the same JVM signature (getSender()Ljava/lang/String;)
public final fun <get-sender>(): String defined in jvmannotation.Message
public final fun getSender(): String defined in jvmannotation.Message
产生这个错误的原因是 Koltin 自动生成了getSender function 所以不能添加相同名称的getSender
使用 @JvmName 解决这个问题
val sender = "me"
@JvmName("getSenderName")
fun getSender() : String = "from:$sender"
Kolitn 使用示例:
val message = Message()
val sender = message.sender
val getSender = message.getSender()
println("sender = ${sender} getSender = ${getSender}")
//sender = me getSender = from:me
Java 使用示例:
Message message = new Message();
String sender = message.getSender();
String getSender = message.getSenderName();
System.out.println("sender = " + sender + " getSender = " + getSender);
//sender = me getSender = from:me
当然我们定义方法的时候,避免定义 getter 和 setter 这样会减少很多问题
3.4、类型擦除分歧
由于类型擦除引起的问题:
fun setReceivers(receiverNames : List<String>) {
}
fun setReceivers(receiverNames : List<Int>) {
}
编译错误
Platform declaration clash: The following declarations have the same JVM signature (setReceivers(Ljava/util/List;)V)
@JvmName 修改
@file:JvmName("MessageHelper")
package jvmannotation
fun setReceivers(receiverNames : List<String>) {
println(receiverNames)
}
@JvmName("setReceiverIds")
fun setReceivers(receiverNames : List<Int>) {
println(receiverNames)
}
Kotlin 调用实例
setReceivers(listOf(1, 2, 3)) //输出 [1, 2, 3]
setReceivers(listOf("1", "2", "3")) //输出 [1, 2, 3]
Java 调用实例
List<String> strs = new ArrayList<>();
strs.add("1");
strs.add("2");
strs.add("3");
MessageHelper.setReceivers(strs); //输出 [1, 2, 3]
List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
ints.add(3);
MessageHelper.setReceiverIds(ints); //输出 [1, 2, 3]
3.5、Getter 和 Setter
通过 @JvmName 修改默认生成的Getter 和 Setter 方法
package jvmannotation
class Message{
var name:String = ""
@get:JvmName("getContent")
@set:JvmName("setContent")
var text:String = ""
}
Kotlin 调用
val message = Message()
message.name = "李四"
val name = message.name
message.text = "content"
val content = message.text
Java 调用
Message message = new Message();
message.setName("李四");
String name = message.getName();
message.setContent("content");
String content = message.getContent();
3.6、命名约定
在从Java调用Kotlin类时,当我们想要遵守某些命名约定时,@JvmName注释也很方便
Kotlin 生成getter方法时,默认会加上前缀,智能的 Kotlin 会对 is 开头 Boolean 类型做自动处理,但是Kotlin 也不是那么智能,比如下面的代码,它会为hasAttachment 也加入了 get 前缀,这样就不符合命名约定了。代码如下
package jvmannotation
class Message{
val isEncrypted: Boolean = false
val hasAttachment: Boolean = false
}
Kotlin 中的调用
val message = Message()
println("${message.isEncrypted.toString()} ${message.hasAttachment}")
Java 中的调用
Message message = new Message();
System.out.println(message.isEncrypted() +" "+message.getHasAttachment());
// message.getHasAttachment() 不符合命名约定
可以通过@JvmName 改写为如下:
package jvmannotation
class Message{
val isEncrypted: Boolean = false
@get:JvmName("hasAttachment")
val hasAttachment: Boolean = false
}
Kotlin 中的调用
val message = Message()
println("${message.isEncrypted.toString()} ${message.hasAttachment}")
Java 中的调用
Message message = new Message();
System.out.println(message.isEncrypted() +" "+message.hasAttachment());
// message.hasAttachment() 符合命名约定
3.7、修改限制
注意,注释只能应用于具有适当访问权限的类成员。
如果我们试图将@set:JvmName添加到不可变成员:
@set:JvmName("setSender")
val sender = "me"
会得到一个编译异常
Error:(11, 5) Kotlin: '@set:' annotations could be applied only to mutable properties
如果我们试图将@get:JvmName或@set:JvmName添加到私有成员:
@get:JvmName("getId")
private id = 0
会得到一个警告,编译器会忽律这个annotation
4. @JvmStatic 和 @JvmField
4.1、@JvmStatic
@JvmStatic注释可以应用于命名Object 修饰的对象或伴随对象的函数或属性。
未注释的 MessageBroker 和 Message :
package jvmannotation
class Message {
companion object {
val isEncrypted: Boolean = false
fun sendMsg() {}
}
}
object MessageBroker {
var totalMessagesSent = 0
fun clearAllMessages() {}
}
在 Kotlin 中访问 :
val isEncrypted = Message.isEncrypted
Message.sendMsg()
val totalMessagesSent = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()
在 Java 中访问(需要通过 Companion 或 INSTANCE) :
boolean isEncrypted = Message.Companion.isEncrypted();
Message.Companion.sendMsg();
int totalMessagesSent = MessageBroker.INSTANCE.getTotalMessagesSent();
MessageBroker.INSTANCE.clearAllMessages();
如果想和Kotlin中的一样通过静态的方式访问需要加入@JvmStatic :
class Message {
companion object {
@JvmStatic
val isEncrypted: Boolean = false
@JvmStatic
fun sendMsg() {}
}
}
object MessageBroker {
@JvmStatic
var totalMessagesSent = 0
@JvmStatic
fun clearAllMessages() {}
}
在 Kotlin 中访问 :
val isEncrypted = Message.isEncrypted
Message.sendMsg()
val totalMessagesSent = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()
在 Java 中访问:
boolean isEncrypted = Message.isEncrypted();
Message.sendMsg();
int totalMessagesSent = MessageBroker.getTotalMessagesSent();
MessageBroker.clearAllMessages();
4.2、@JvmField
@JvmField 用在 顶层、类、companion、object class 属性,不生产 Getter 和Setter 方法
未使用 @JvmField 的示例:
package jvmannotation
var num = 0
class Message {
var isHasMsg: Boolean = false
companion object {
val isEncrypted: Boolean = false
}
}
object MessageBroker {
var totalMessagesSent = 0
}
在 Kotlin 中访问 :
val numIn = num
val isEncrypted = Message.isEncrypted
val isHasMsg = Message().isHasMsg
val totalMessagesSent = MessageBroker.totalMessagesSent
在 Java 中访问 :
int numIn = MessageKt.getNum();
boolean isEncrypted = Message.Companion.isEncrypted();
boolean isHasMsg = (new Message()).isHasMsg();
int totalMessagesSent = MessageBroker.INSTANCE.getTotalMessagesSent();
使用 @JvmField 的示例:
package jvmannotation
@JvmField
var num = 0
class Message {
@JvmField
var isHasMsg: Boolean = false
companion object {
@JvmField
val isEncrypted: Boolean = false
}
}
object MessageBroker {
@JvmField
var totalMessagesSent = 0
}
在 Kotlin 中访问 :
val numIn = num
val isEncrypted = Message.isEncrypted
val isHasMsg = Message().isHasMsg
val totalMessagesSent = MessageBroker.totalMessagesSent
在 Java 中访问 :
val numIn = num
val isEncrypted = Message.isEncrypted
val isHasMsg = Message().isHasMsg
val totalMessagesSent = MessageBroker.totalMessagesSent
4.3、@JvmField, @JvmStatic 和 Constants
为了更好理解 @JvmField, @JvmStatic 和 constant ,查看下面的代码:
object MessageBroker {
@JvmStatic
var totalMessagesSent = 0
@JvmField
var maxMessagePerSecond = 0
const val maxMessageLength = 0
}
由 object 实现的单例模式,被编译成了下面的 class ,包含一个私有构造器和 static INSTANCE 相当于下面的代码
public final class MessageBroker {
private static int totalMessagesSent = 0;
public static int maxMessagePerSecond = 0;
public static final int maxMessageLength = 0;
public static MessageBroker INSTANCE = new MessageBroker();
private MessageBroker() {
}
public static int getTotalMessagesSent() {
return totalMessagesSent;
}
public static void setTotalMessagesSent(int totalMessagesSent) {
this.totalMessagesSent = totalMessagesSent;
}
}
通过 @JvmStatic 修饰的属性 相当于 private static field 并提供了 getter 和 setter 方法,@JvmField 修饰的属性相当于 public static field ,const val 修饰的属性相当于public static final field
5、 @JvmOverloads
在 kotlin 中我们可以为方法提供默认的值,这样就减少了overloads的方法,同时保持简洁的调用
例如下面的代码
object MessageBroker {
@JvmStatic
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
return ArrayList()
}
}
在 Kotlin 中可以这样调用
MessageBroker.findMessages("me", "text", 5);
MessageBroker.findMessages("me", "text");
MessageBroker.findMessages("me");
在 Java 中调用确需要提供默认值
MessageBroker.findMessages("me", "text", 10);
通过@JvmOverloads 可以解决这个问题
下面的 kotlin 代码
@JvmStatic
@JvmOverloads
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
return ArrayList()
}
相当于 Java代码如下
public static List<Message> findMessages(String sender, String type, int maxResults)
public static List<Message> findMessages(String sender, String type)
public static List<Message> findMessages(String sender)
6、@JvmDefault
与默认接口有关
7. @Throws
kotlin 不检查异常,也就是说 try-catch 是可选的
7.1、 Exceptions in Kotlin
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
if(sender.isEmpty()) {
throw IllegalArgumentException()
}
return ArrayList()
}
在kotlin 中调用
MessageBroker.findMessages("me")
try {
MessageBroker.findMessages("me")
} catch(e : IllegalArgumentException) {
}
//try-catch 是可选操作
在Java中调用
MessageBroker.findMessages("");
try {
MessageBroker.findMessages("");
} catch (Exception e) {
e.printStackTrace();
}
//try-catch 是可选操作
7.1、 创建 Java 必检查的 异常
@Throws(Exception::class)
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
if(sender.isEmpty()) {
throw IllegalArgumentException()
}
return ArrayList()
}
// MessageBroker.findMessages(""); Unhandled exception: java.lang.Exception
try {
MessageBroker.findMessages("");
} catch (Exception e) {
e.printStackTrace();
}
//try-catch 是必选操作
8. @JvmWildcard 和 @JvmSuppressWildcards
8.1 通配符
在 Java 中,我们需要通配符来处理继承中的泛型,即使 Integer extends Number,下面的代码也会导致编译错误
List<Number> numberList = new ArrayList<Integer>();
我们可以通过通配符的方式解决这个问题
List<? extends Number> numberList = new ArrayList<Integer>();
在 kotlin 中,没有通配符,我可以简写
val numberList : List<Number> = ArrayList<Long>() // 不会报错因为 Kotlin 中的 List 是不可变的,符合 PECS
// val list:MutableList<Number> = ArrayList<Long>() 会报错因为 Kotlin 中的 MutableList 是可变的,不符合 PECS
这就引出了一个问题:如果我们使用包含这样一个列表的Kotlin类,会发生什么。
例如,我们来看一个以列表为参数的函数:
kotlin 中定义的方法
package jvmannotation
fun transformList(list : List<Number>) : List<Number>{
return list
}
在 Kotlin 中,我们可以使用任何 extend Number 的参数调用此函数:
val numbers1 = transformList(listOf(1,2,3))
println(numbers1) //[1, 2, 3]
// val longsError:List<Long> = transformList(listOf(1,2,3)) 报错
val longs: List<Long> = mutableListOf(1L,2L,3L)
val numbers2 = transformList(longs)
println(longs.hashCode().toString() + " : " + numbers2.hashCode()) //30817 : 30817
val numbers3 = transformList(longs).toMutableList()
println(numbers3) //[1, 2, 3]
numbers3.add(1.2)
println(numbers3) //[1, 2, 3, 1.2]
println(longs) //[1, 2, 3]
println(longs.hashCode().toString() + " : " + numbers3.hashCode()) //30817 : 214864831
同样在 Java 中调用
List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
ints.add(3);
List<Number> numbers1 = MessageKt.transformList(ints);
System.out.println(numbers1); //[1, 2, 3]
//List<Long> longs1 = MessageKt.transformList(longs); 报错
List<Long> longs = new ArrayList<>();
longs.add(1L);
longs.add(2L);
longs.add(3L);
List<Number> numbers2 = MessageKt.transformList(longs);
System.out.println(numbers2); //[1, 2, 3]
numbers2.add(1.2);
System.out.println(numbers2); //[1, 2, 3, 1.2]
System.out.println(longs); //[1, 2, 3, 1.2]
System.out.println(longs.hashCode() + " : "+numbers2.hashCode()); //214864831 : 214864831
从上面的 Java 代码中可以看到,Java的调用也没问题,从Java的角度来看 ,函数的实现如下
public List<Number> transformList(List<? extends Number> list)
在 kotlin 编译的时会含蓄的为方法创建一个泛型
8.2 Koltin 的 通配符规则
最基本的规则 :Kotlin仅在必要时生成通配符
如果类型参数是final class,则不存在通配符:
fun transformList(list : List<String>) // Kotlin
public void transformList(List<String> list) // Java
就像上面的代码 不需要 " ? extends Number ", 因为String 是 final class ,但是当一个类能被继承,就会生成通配符,Number 不是 final class,所以有通配符
fun transformList(list : List<Number>) // Kotlin
public void transformList(List<? extends Number> list) // Java
当然返回值不包含通配符
fun transformList() : List<Number> // Kotlin
public List<Number> transformList() // Java
8.3 通配符的结构
但是,在某些情况下,我们需要修改默认的行为。 可以使用JVM annotations。JvmWildcard 保证 被注释的参数会生成泛型。JvmSuppressWildcards 保证被修饰的参数不会生成泛型:
fun transformList(list : List<@JvmSuppressWildcards Number>) : List<@JvmWildcard Number> // Kotlin
public List<? extends Number> transformListInverseWildcards(List<Number> list) // Java
最后,我们必须注意到在 Java 中,返回类型中的通配符通常是不好的做法,但是某些情况下我们需要这么做。这时候 kotlin JVM annotations 就派上了用场。
9. @JvmMultifileClass
我们已经看到将 @JvmName 注释于文件,使定义的顶级声明编译到指定类里。当然我们提供名称必须是唯一的。
假设我们在同一个包中有两个Kotlin文件,都带有@JvmName注释和相同的目标类名。第一个文件MessageConverter。kt,代码如下:
@file:JvmName("MessageHelper")
package jvmannotation
convert(message: Message) = // conversion code
第二个类 Message.kt 有如下代码:
@file:JvmName("MessageHelper")
package jvmannotation
fun archiveMessage() = // archiving code
如果我们这样做,会出现一个错误
// Error:(1, 1) Kotlin: Duplicate JVM class name 'jvmannotation/MessageHelper'
// generated from: package-fragment jvmannotation, package-fragment jvmannotation
出现这错误的原因,是因为 kotlin 编译器尝试创建两个同名的class。
如果想把两个文件里的顶层定义放在同一个文件里面,可以在两个文件都加入@JvmMultifileClass
在 MessageConverter.kt 中加入 @JvmMultifileClass
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotationfun
convert(message: Message) = // conversion code
在 Message.kt 也加入@JvmMultifileClass
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotation
fun archiveMessage() =
这样在Java 中你可以通过MessageHelper 访问所有两个文件的顶层测试
MessageHelper.archiveMessage();
MessageHelper.convert(new Message());
这个注释不在 kotlin 调用时起作用
10. @ JvmPackageName
所有的JVM annotation 都定义在 kotlin.jvm 包中,在这个包里面还有一个注释 @JvmPackageName
这个 annotation 可以改变package 的名字,与@file:JvmName 生成 class 文件相似
然而,这个annotation 被标记为内部,这意味着他不能再 kotlin 类库之外使用,所有不再做详细的介绍
11、注册备忘单
官方文档 能够查找到所有 JVM annotations。可以在源码中更详细的查看。 他的定义(包括文档)可以在 kotlin-stdlib.jar 中的 kotlin.jvm 包中查看。
下面这个图表,展示 annotation 可以用到的位置