前言
Kotlin
为了能和Java
更加友好的进行交互(PY),提供了一些注解参数使得Java调用Kotlin时更加方便和友好.
今天我们来学习和理解这些常用的注解:JvmDefault
JvmField
JvmMultifileClass
JvmName
JvmOverloads
JvmStatic
Strictfp
Synchronized
Volatile
Transient
JvmDefault
指定为非抽象Kotlin接口成员生成JVM默认方法。 此注解的用法需要指定编译参数: -Xjvm-default=enable
或者-Xjvm-default=compatibility
。
-Xjvm-default=enable
:仅为每个@JvmDefault
方法生成接口中的默认方法。在此模式下,使用@JvmDefault
注释现有方法可能会破坏二进制兼容性,因为它将有效地从DefaultImpls
类中删除该方法。-Xjvm-default=compatibility
:除了默认的接口方法,在生成的兼容性访问DefaultImpls
类,调用通过合成访问默认接口方法。在此模式下,使用@JvmDefault
注释现有方法是二进制兼容的,但在字节码中会产生更多方法。
从接口成员中移除此注解会使在两种模式中的二进制不兼容性发生变化。
只有JVM目标字节码版本1.8(-jvm-target 1.8
)或更高版本才能生成默认方法。
让我们试一试这个注解看看怎么使用
首先我们看不加注解的情况:
interface Animal { var name: String? var age: Int? fun getDesc() = name + "今年已经" + age + "岁啦~"}class Dog(override var name: String?, override var age: Int?) : Animalfun main(args: Array?) { Dog("小白", 3).getDesc()}复制代码
字节码转换成Java代码以后是下面这个样子:
public interface Animal { @Nullable String getName(); void setName(@Nullable String var1); @Nullable Integer getAge(); void setAge(@Nullable Integer var1); @NotNull String getDesc(); public static final class DefaultImpls { @NotNull public static String getDesc(Animal $this) { return $this.getName() + "今年已经" + $this.getAge() + "岁啦~"; } }}public final class Dog implements Animal { @Nullable private String name; @Nullable private Integer age; @Nullable public String getName() { return this.name; } public void setName(@Nullable String var1) { this.name = var1; } @Nullable public Integer getAge() { return this.age; } public void setAge(@Nullable Integer var1) { this.age = var1; } public Dog(@Nullable String name, @Nullable Integer age) { this.name = name; this.age = age; } @NotNull public String getDesc() { return Animal.DefaultImpls.getDesc(this); }}public final class JvmKt { public static final void main(@Nullable String[] args) { (new Dog("小白", 3)).getDesc(); }}复制代码
从上述代码可以发现,当我们去调用接口的默认方法时,其实是调用了匿名静态内部类的方法。
Kotlin创建了一个静态内部类,调用DefaultImpls
它来存储方法的默认实现,这些方法都是静态的,并使用 “Self” 接收器类型来模拟属于对象的方法。然后,对于扩展该接口的每种类型,如果类型没有实现方法本身,则在编译时,Kotlin将通过调用将方法连接到默认实现。
这样的实现有好处也有坏处,好处是它可以在Java 8之前的JVM上也能在接口上提供具体方法的强大功能,缺点是:
- 它与Java的处理方式不兼容,导致了互操作性很混乱.我们可以在Java代码中直接调用
DefaultImpls
类,这是一个很神奇的事情.... - Java8中存在默认方法的主要原因之一是能够在不必触及每个子类(例如添加
Collection.stream()
)的情况下向接口添加方法.Kotlin实现不支持这个,因为必须在每个具体类型上生成默认调用。向接口添加新方法导致必须重新编译每个实现者.
为了解决这个问题,Kotlin推出了@JvmDefault
来优化这种情况. 接下来我们看看加注解以后是什么样子: gradle文件添加编译配置
-jvm-target=1.8 -Xjvm-default=enable复制代码
Kotlin代码
interface Animal { var name: String? var age: Int? @JvmDefault fun getDesc() = name + "今年已经" + age + "岁啦~"}class Dog(override var name: String?, override var age: Int?) : Animalfun main(args: Array?) { Dog("小白", 3).getDesc()}复制代码
对应的Java代码
public interface Animal { @Nullable String getName(); void setName(@Nullable String var1); @Nullable Integer getAge(); void setAge(@Nullable Integer var1); @JvmDefault @NotNull default String getDesc() { return this.getName() + "今年已经" + this.getAge() + "岁啦~"; }}public final class Dog implements Animal { @Nullable private String name; @Nullable private Integer age; @Nullable public String getName() { return this.name; } public void setName(@Nullable String var1) { this.name = var1; } @Nullable public Integer getAge() { return this.age; } public void setAge(@Nullable Integer var1) { this.age = var1; } public Dog(@Nullable String name, @Nullable Integer age) { this.name = name; this.age = age; }}public final class JvmKt { public static final void main(@Nullable String[] args) { (new Dog("小白", 3)).getDesc(); }}复制代码
我们可以看到使用注解以后消除了匿名静态内部类去桥接实现默认方法,这样的话,Java调用Kotlind接口的默认方法时就和调用Java接口的默认方法基本一致了.
注意,除了更改编译器标志外,还使用Kotlin 1.2.50
添加了兼容模式。兼容性标志(-Xjvm-default=compatibility
)专门用于保留与现有Kotlin类的二进制兼容性,同时仍然能够转移到Java 8样式的默认方法。在考虑生成的指向静态桥接方法的其他项目时,此标志特别有用。
为实现此目的,Kotlin编译器使用类文件技巧invokespecial
来调用默认接口方法,同时仍保留DefaultImpls
桥接类。我们一起来看看是什么样子的:
public interface Animal { @JvmDefault @NotNull default String getDesc() { return "喵喵喵喵"; } public static final class DefaultImpls { @NotNull public static String getDesc(Animal $this) { return $this.getDesc(); } }}//新编译的情况下public final class Dog implements Animal {}//在其他一些项目中,已经编译存在public final class OldDog implements Animal { @NotNull public String getDesc() { return Animal.DefaultImpls.getDesc(this); }}复制代码
这里有一个很好的解压缩,特别是因为这不是有效的Java语法。以下是一些注意事项:
- 在接口上生成默认方法,就像我们刚刚使用时一样
enable
- 新编译的类,如
Dog
,将直接使用Java 8样式的默认接口。 - 像
OldDog
这样的现有编译代码仍然可以在二进制级别工作,因为它指向了DefaultImpls类。 - 该
DefaultImpls
方法的实现不能在真正的Java源来表示,因为它是类似于调用<init>
或<super>
; 该方法必须在提供的实例上调用Animal接口上的getDesc方法。如果它只是调用“getDesc()”,它将导致旧类型(OldDog.getDesc()
->DefaultImpls.getDesc()
->OldDog.getDesc()
)的堆栈溢出。相反,它必须直接调用接口:OldDog.getDesc()
->DefaultImpls.getDesc()
->Animal.getDesc()
,并且只能通过接口方法invokespecial
调用来完成.
JvmField
使Kotlin编译器不再对该字段生成getter/setter
并将其作为公开字段(public)
使用场景如下:
kotlin代码
class Bean( @JvmField var name:String?, var age:Int)复制代码
对应的Java代码
public final class Bean { @JvmField @Nullable public String name; private int age; public final int getAge() { return this.age; } public final void setAge(int var1) { this.age = var1; } public Bean(@Nullable String name, int age) { this.name = name; this.age = age; }}复制代码
对比很明显,被注解的字段属性修饰符会从private
变成public
JvmName
这个注解的主要用途就是告诉编译器生成的Java类或者方法的名称
使用场景如下:
Koltin代码
@file:JvmName("JavaClass")package com.example.maqiang.sssvar kotlinField: String? = null //修改属性的set方法名 @JvmName("setJavaField") set(value) { field = value }//修改普通的方法名@JvmName("JavaFunction")fun kotlinFunction() {}复制代码
对应的Java代码:
public final class JavaClass { @Nullable private static String kotlinField; @Nullable public static final String getKotlinField() { return kotlinField; } @JvmName( name = "setJavaField" ) public static final void setJavaField(@Nullable String value) { kotlinField = value; } @JvmName( name = "JavaFunction" ) public static final void JavaFunction() { }}复制代码
Java调用kotlin代码
public class JavaJvm{ public static void main(String[] args) { //类名和方法都是注解修改以后的 JavaClass.JavaFunction(); JavaClass.getKotlinField(); JavaClass.setJavaField("java"); }}复制代码
这个注解我们用来应对各种类名修改以后的兼容性问题
JvmMultifileClass
这个注解让Kotlin编译器生成一个多文件类,该文件具有在此文件中声明的顶级函数和属性作为其中的一部分,JvmName
注解提供了相应的多文件的名称.
使用场景解析:
Kotlin代码:
//A.kt@file:JvmName("Utils")@file:JvmMultifileClasspackage com.example.maqiang.sssfun getA() = "A"//B.kt@file:JvmName("Utils")@file:JvmMultifileClasspackage com.example.maqiang.sssfun getB() = "B"复制代码
Java调用Kotlin的顶级函数
public class JavaJvm { public static void main(String[] args) { Utils.getA(); Utils.getB(); }}复制代码
我们可以看到使用注解以后将A和B文件中的方法合在了一个Utils
类中,这个注解可以消除我们去手动创建一个Utils类,向Utils类中添加方法更加灵活和方便
JvmOverloads
告诉Kotlin编译器为此函数生成替换默认参数值的重载
使用场景如下:
kotlin代码
@JvmOverloadsfun goToActivity( context: Context?, url: String?, bundle: Bundle? = null, requestCode: Int = -1) {}复制代码
对应的Java代码
public final class AKt { @JvmOverloads public static final void goToActivity(@Nullable Context context, @Nullable String url, @Nullable Bundle bundle, int requestCode) { } // $FF: synthetic method // $FF: bridge method @JvmOverloads public static void goToActivity$default(Context var0, String var1, Bundle var2, int var3, int var4, Object var5) { if ((var4 & 4) != 0) { var2 = (Bundle)null; } if ((var4 & 8) != 0) { var3 = -1; } goToActivity(var0, var1, var2, var3); } @JvmOverloads public static final void goToActivity(@Nullable Context context, @Nullable String url, @Nullable Bundle bundle) { goToActivity$default(context, url, bundle, 0, 8, (Object)null); } @JvmOverloads public static final void goToActivity(@Nullable Context context, @Nullable String url) { goToActivity$default(context, url, (Bundle)null, 0, 12, (Object)null); }复制代码
我们可以看到为了能让Java享受到Koltin的默认参数的特性,使用此注解来生成对应的重载方法。
重载的规则是顺序重载,只有有默认值的参数会参与重载.
JvmStatic
对函数使用该注解,kotlin编译器将生成另一个静态方法
对属性使用该注解,kotlin编译器将生成其他的setter和getter方法
这个注解的作用其实就是消除Java调用Kotlin的companion object
对象时不能直接调用其静态方法和属性的问题.
使用场景对比
companion object
中未使用注解的情况下
class A { companion object { var string: String? = null fun hello() = "hello,world" }}复制代码
对应的Java代码
public final class A { @Nullable private static String string; public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null); public static final class Companion { @Nullable public final String getString() { return A.string; } public final void setString(@Nullable String var1) { A.string = var1; } @NotNull public final String hello() { return "hello,world"; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } }}复制代码
我们可以看到这个时候Java去调用kotlin的伴生对象的方法和属性时候需要通过Companion
.
companion object
中使用注解的情况下
class A { companion object { @JvmStatic var string: String? = null @JvmStatic fun hello() = "hello,world" }}复制代码
对应的Java代码
public final class A { @Nullable private static String string; public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null); @Nullable public static final String getString() { A.Companion var10000 = Companion; return string; } public static final void setString(@Nullable String var0) { A.Companion var10000 = Companion; string = var0; } @JvmStatic @NotNull public static final String hello() { return Companion.hello(); } public static final class Companion { /** @deprecated */ // $FF: synthetic method @JvmStatic public static void string$annotations() { } @Nullable public final String getString() { return A.string; } public final void setString(@Nullable String var1) { A.string = var1; } @JvmStatic @NotNull public final String hello() { return "hello,world"; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } }复制代码
我们可以看到,虽然Companion
这个静态内部类还在,但是Java现在可以直接调用对应的静态方法和属性了.
注解使用前和注解使用后的Java调用对比
public class JavaJvm { public static void main(String[] args) { //使用注解前 A.Companion.hello(); A.Companion.getString(); A.Companion.setString("hello,kotlin"); //使用注解后 A.hello(); A.getString(); A.setString("hello,kotlin"); }}复制代码
明显注解使Java和kotlin的交互更加友好了~
Strictfp
将从注释函数生成的JVM方法标记为strictfp
,意味着需要限制在方法内执行的浮点运算的精度,以实现更好的可移植性。
对应Java中的strictfp
关键字
使用场景如下:
//可以用在构造函数、属性的getter/setter、普通方法//官网的Target中有class,但是实际使用并不能对class加注解class JvmAnnotation @Strictfp constructor() { var a: Float = 0.0f @Strictfp get() { return 1f } @Strictfp set(value) { field = value } @Strictfp fun getFloatValue(): Float = 0.0f }复制代码
Synchronized
将从带注释的函数生成的JVM方法标记为synchronized
,这意味着该方法将受到定义该方法的实例(或者对于静态方法,类)的监视器的多个线程的并发执行的保护。
对应Java中的synchronized
关键字
使用场景如下
class JvmAnnotation { var syn: String = "" @Synchronized get() { return "test" } @Synchronized set(value) { field = value } @Synchronized fun getSynString(): String = "test" fun setSynString(str:String){ //注意这里使用的是内敛函数来实现的对代码块加锁 synchronized(this){ println(str) } }}复制代码
Volatile
将带注释属性的JVM支持字段标记为volatile,这意味着对此字段的写入立即对其他线程可见.
对应Java中的volatile
关键字
使用场景如下
//不能对val变量加注解@Volatilevar volatileStr: String = "volatile"复制代码
Transient
将带注释的属性的JVM支持字段标记为transient
,表示它不是对象的默认序列化形式的一部分。
对应Java中的transient
关键字
使用场景如下:
//:Serializabledata class XBean( val name: String?, val age: Int?, //不参与序列化 @Transient val male: Boolean = true): Serializable//Parcelize(目前还是实验性功能 需要在gradle中配置开启 experimental = true)@Parcelizedata class XBean( val name: String?, val age: Int?, //不参与序列化 @Transient val male: Boolean = true)复制代码
以上就是日常开发过程中最常用到的一些注解,如果你有疑问欢迎留言交流~~