美文网首页kotlin
Kotlin JVM常用注解参数解析

Kotlin JVM常用注解参数解析

作者: 旅旅人 | 来源:发表于2018-10-23 23:15 被阅读625次
    前言

    Kotlin为了能和Java更加友好的进行交互(PY),提供了一些注解参数使得Java调用Kotlin时更加方便和友好.

    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?) : Animal
    
    fun main(args: Array<String>?) {
      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?) : Animal
    
    fun main(args: Array<String>?) {
       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.sss
    
    var 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:JvmMultifileClass
    package com.example.maqiang.sss
    fun getA() = "A"
    
    //B.kt
    @file:JvmName("Utils")
    @file:JvmMultifileClass
    package com.example.maqiang.sss
    fun 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代码

    @JvmOverloads
    fun 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中使用

    使用场景对比

    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变量加注解
    @Volatile
    var volatileStr: String = "volatile"
    

    Transient

    将带注释的属性的JVM支持字段标记为transient,表示它不是对象的默认序列化形式的一部分。

    对应Java中的transient关键字

    使用场景如下:

    //:Serializable
    data class XBean(
        val name: String?,
        val age: Int?,
        //不参与序列化
        @Transient
        val male: Boolean = true
    ): Serializable
    
    //Parcelize(目前还是实验性功能 需要在gradle中配置开启 experimental = true)
    @Parcelize
    data class XBean(
        val name: String?,
        val age: Int?,
        //不参与序列化
        @Transient
        val male: Boolean = true
    )
    

    以上就是日常开发过程中最常用到的一些注解,如果你有疑问欢迎留言交流~~

    相关文章

      网友评论

        本文标题:Kotlin JVM常用注解参数解析

        本文链接:https://www.haomeiwen.com/subject/shvzzftx.html