JVM平台指南 Kotlin 中的 Annotations

原文档 https://www.baeldung.com/kotlin/jvm-annotations

1、介绍

kotlin 提供了几个 annotations 用于 Kotlin 和 Java 更好的交互,在这个指南,我们将着重介绍 Kotlin 的 JVM annotations 的使用方法,以及他们的作用


JVM_Annotations.png

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 可以用到的位置

kotlin-jmv-anns.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容