Kotlin Jvm Annotations

Kotlin 中删除了静态的概念,通过伴生对象 (companion object)来实现静态能力。但实际上在 Java 中调用是与传统的静态调用方式不一样的。例如,下面这个类在 java 中是这样的:

public class StaticCheckJava {
    static String name = "";
    static boolean check() {
        return false;
    }
}

用 Kotlin 来实现,应该是这样的:

class StaticCheck {
    companion object {
        var name: String = 0
        fun check()Boolean {
            return false
        }
    }
}

但是,这个简单的 demo 用 IDE 自动转换却成了这样:

object StaticCheckObject {
    var name = ""
    fun check(): Boolean {
        return false
    }
}

好吧,那么就看看他们用起来有什么区别吧。

在 Kotlin 中使用,是没有区别的:

fun main() {
    val kotlinStaticName = StaticCheck.name
    val kotlinStaticMethod = StaticCheck.check()
    
    val kotlinStaticNameObj = StaticCheckObject.name
    val kotlinStaticMethodObj = StaticCheckObject.check()
    
    val kotlinStaticNameJava = StaticCheckJava.name
    val kotlinStaticMethodJava = StaticCheckJava.check()
}

但来到 Java 中就出现了差异:

public static void main(String[] args) {
    String kotlinStaticName = StaticCheck.Companion.getName();
    boolean kotlinStaticMethod = StaticCheck.Companion.check();

    String kotlinStaticNameObj = StaticCheckObject.INSTANCE.getName();
    boolean kotlinStaticMethodObj = StaticCheckObject.INSTANCE.check();

    String kotlinStaticNameJava = StaticCheckJava.name;
    boolean kotlinStaticMethodJava = StaticCheckJava.check();
}

三种情况都不一样:

  • Kotlin companion object :在 Java 中使用,需要通过 Companion 来调用属性和方法,并且属性是通过 getter 方法调用的。
  • Kotlin object:默认生成了 INSTANCE ,通过 INSTANCE 调用属性和方法,并且属性是通过 getter 方法调用的。
  • Java static:直接调用。

既然三种调用都不相同,说明编译器生成的 class 文件内容肯定也不一样。那就分别看看它们产生的字节码到底是什么样子的。

Java static

public class com.chunyu.check.StaticCheckJava {

  static java.lang.String name;

  public com.chunyu.check.StaticCheckJava();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4return

  static boolean check();
    Code:
       0: iconst_0
       1: ireturn

  static {};
    Code:
       0: ldc           #2                  // String
       2: putstatic     #3                  // Field name:Ljava/lang/String;
       5return
}

StaticCheckJava 反编译后十分简单, name 和 check() 方法都是通过 static 修饰,并且也只是有一个构造方法而已。

Kotlin object

Kotlin 中只能通过查看 kotlin 字节码,然后反编译成 Java 的形式查看。

public final class StaticCheckObject {
   @NotNull
   private static String name;

   @NotNull
   public static final StaticCheckObject INSTANCE;

   @NotNull
   public final String getName() {
      return name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      name = var1;
   }

   public final boolean check() {
      return false;
   }

   private StaticCheckObject() {
   }

   static {
      StaticCheckObject var0 = new StaticCheckObject();
      INSTANCE = var0;
      name = "";
   }
}

Kotlin 单例关键字 object 生成的单例是个简单的静态单例模式。

Kotlin companion object

public final class StaticCheck {

    @NotNull
    private static String name = "";

    @NotNull
    public static final StaticCheck.Companion Companion = new StaticCheck.Companion((DefaultConstructorMarker)null);

    public static final class Companion {
        @NotNull
        public final String getName() {
            return StaticCheck.name;
        }

        public final void setName(@NotNull String var1) {
            Intrinsics.checkNotNullParameter(var1, "<set-?>");
            StaticCheck.name = var1;
        }

        public final boolean check() {
             return false;
        }

        private Companion() {
        }

        // $FF: synthetic method
        public Companion(DefaultConstructorMarker $constructor_marker) {
            this();
        }
    }
}

伴生对象生成了一个静态内部类 Companion ,然后通过这个 Companion 来访问类自身的属性。

三者的实现各不相同,但是在 Kotlin 中,调用方式确实相同的,Kotlin 作为一个高级语言,隐藏了很多细节。

@JvmStatic

从上面的三种情况的不同可以引出一个 Java 与 Kotlin 互操作的问题,如果你的项目之前是 Java 编写的,在迁移到 Kotlin 时,有一个类有一些静态属性和方法,此时你通过 IDE 自动转换,Java 中的 static 修饰的内容转换成了 companion object 代码块中的内容,此时编译项目,原来 Java 中调用这些静态方法的地方都会出现错误,因为调用方式发生了变化:

// java 静态方法的调用方式
A.method()

// 转换成 kotlin 后,在 java 中调用方式
A.Companion.method()

有没有什么办法保证在 Java 中调用方式与 Java 静态方法的调用一致呢?通过**@JvmStatic** 注解就可以实现。

修改 StaticCheck 的内容:

class StaticCheck {
    companion object {
        @JvmStatic
        var name: String = ""

        @JvmStatic
        fun check()Boolean {
            return false
        }
    }
}

反编译成 Java:

public final class StaticCheck {

    @NotNull
   private static String name = "";

   @NotNull
   public static final StaticCheck.Companion Companion = new StaticCheck.Companion((DefaultConstructorMarker)null);

   @NotNull
   public static final String getName() {
      StaticCheck.Companion var10000 = Companion;
      return name;
   }

   public static final void setName(@NotNull String var0) {
      StaticCheck.Companion var10000 = Companion;
      name = var0;
   }

   @JvmStatic
   public static final boolean check() {
      return Companion.check();
   }

   public static final class Companion {

      @JvmStatic
      public static void getName$annotations() {
      }

      @NotNull
      public final String getName() {
         return StaticCheck.name;
      }

      public final void setName(@NotNull String var1) {
         Intrinsics.checkNotNullParameter(var1, "<set-?>");
         StaticCheck.name = var1;
      }

      @JvmStatic
      public final boolean check() {
         return false;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

相较于之前不加 @JvmStatic ,多了三个静态方法:

   @NotNull
   public static final String getName() {
      StaticCheck.Companion var10000 = Companion;
      return name;
   }

   public static final void setName(@NotNull String var0) {
      StaticCheck.Companion var10000 = Companion;
      name = var0;
   }

   @JvmStatic
   public static final boolean check() {
      return Companion.check();
   }

虽然多了外部类自己的静态方法,但他们只是代理 Companion 的调用。

@JvmStatic 的作用是,指定 Kotlin 编译器,如果它修饰的是一个函数,则生成一个额外的静态方法。如果是属性,则应生成其他静态 getter/setter 方法。

@JvmStatic 只能在 companion object 代码块中或 object class 中使用。

@JvmField

在上面的例子中,Kotlin 类在 Java 中访问属性,都是通过 getter/setter 进行的,那么如果要让访问属性的调用和 Java 一致,直接通过属性调用,有没有什么办法呢?

还是使用 Jvm 注解,通过 @JvmField 修饰属性,就可以让编译器不生成 getter/setter :

class StaticCheck {
    companion object {
        @JvmField
        var name: String = ""

        @JvmStatic
        fun check(): Boolean {
            return false
        }
    }
}

反编译:

public final class StaticCheck {

    @JvmField
   @NotNull
   public static String name = "";

   @NotNull
   public static final StaticCheck.Companion Companion = new StaticCheck.Companion((DefaultConstructorMarker)null);

   @JvmStatic
   public static final boolean check() {
      return Companion.check();
   }

   public static final class Companion {
    @JvmStatic
      public final boolean check() {
         return false;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

在不加 @JvmField 的时候,反编译出来的 name 是 private 的,对外访问的方法是在 Companion 中的 getName/setName 。

而 加了 @JvmField 注解,name 直接是 StaticCheck 的静态 public 属性,Companion 没有任何与 name 相关的内容。

另一个方面,@JvmField 还可以在 companion object 代码块外部使用:

class StaticCheck {
    @JvmField
    var lastName: String = ""
}
public final class StaticCheck {
   @JvmField
   @NotNull
   public String lastName = "";
}

这样使用,并不会将属性编译为 static ,只是普通的属性,作用是阻止编译器自动生成 getter/setter 。

@JvmField 的作用是指示 Kotlin 编译器不要为此属性生成 getter/setter 并将其作为字段公开。

@JvmName

通过 JvmName 可以将 Kotlin 的方法在 Java 中更名为另一个名字,例如:

class StaticCheck {
    companion object {
        @JvmName("javaCheck")
        fun check(): Boolean {
            return false
        }
    }
}

反编译后:

public final class StaticCheck {
   @NotNull
   public static final StaticCheck.Companion Companion = new StaticCheck.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      @JvmName(
         name = "javaCheck"
      )
      public final boolean javaCheck() {
         return false;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

此时,在 Kotlin 中使用,仍是 StaticCheck.check() ,但在 Java 中这个方法的名字已经变成了 javaCheck :

boolean kotlinStaticMethod = StaticCheck.Companion.javaCheck();

@JvmOverloads

@JvmOverloads 常见于 Android 继承一些 Layout 时自动生成的 Kotlin 代码,它的作用是:指示 Kotlin 编译器为此函数生成替换默认参数值的重载。如果一个方法有 N 个参数并且其中 M 个具有默认值,则生成 M 个重载:第一个采用 N-1 个参数(除了最后一个采用默认值之外的所有参数),第二个采用 N-2 个参数,依此类推 上。

举例说明:

open class A(name: String, age: Int, id: String?)

class B @JvmOverloads constructor(name: String, age: Int = 0, id: String? = null): A(name, age, id)

反编译:

public class A {
   public A(@NotNull String name, int age, @Nullable String id) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
   }
}
// B.java
public final class B extends A {

   @JvmOverloads
   public B(@NotNull String name, int age, @Nullable String id) {
      Intrinsics.checkNotNullParameter(name, "name");
      super(name, age, id);
   }

   public B(String var1, int var2, String var3, int var4, DefaultConstructorMarker var5) {
      if ((var4 & 2) != 0) {
         var2 = 0;
      }

      if ((var4 & 4) != 0) {
         var3 = (String)null;
      }

      this(var1, var2, var3);
   }

   @JvmOverloads
   public B(@NotNull String name, int age) {
      this(name, age, (String)null4, (DefaultConstructorMarker)null);
   }

   @JvmOverloads
   public B(@NotNull String name) {
      this(name, 0, (String)null6, (DefaultConstructorMarker)null);
   }
}

@JvmOverloads 修饰构造函数 constructor(name: String, age: Int = 0, id: String? = null) ,反编译后生成了三个构造方法:

    @JvmOverloads
    public B(@NotNull String name, int age, @Nullable String id) {
       Intrinsics.checkNotNullParameter(name, "name");
       super(name, age, id);
    }

    @JvmOverloads
    public B(@NotNull String name, int age) {
       this(name, age, (String)null4, (DefaultConstructorMarker)null);
    }

    @JvmOverloads
    public B(@NotNull String name) {
       this(name, 0, (String)null6, (DefaultConstructorMarker)null);
    }

而如果不使用这个注解:

class B (nameStringageInt 0, id: String? = null): A(name, age, id)

反编译:

public final class B extends A {
   public B(@NotNull String name, int age, @Nullable String id) {
      Intrinsics.checkNotNullParameter(name, "name");
      super(name, age, id);
   }

   // $FF: synthetic method
   public B(String var1, int var2, String var3, int var4, DefaultConstructorMarker var5) {
      if ((var4 & 2) != 0) {
         var2 = 0;
      }

      if ((var4 & 4) != 0) {
         var3 = (String)null;
      }

      this(var1, var2, var3);
   }
}

只有一个三个参数的构造方法。这里的 synthetic method 不是供外部使用的。

其他 JVM 注解

还有一些不常用的注解,这里简单介绍一下。

@JvmMultifileClass

指示 Kotlin 编译器生成一个多文件类,该类具有在此文件中声明的顶级函数和属性作为其组成部分之一。对应的多文件类的名称由 JvmName 注解提供。

@JvmPackageName

更改从此文件生成的 .class 文件的 JVM 包的完全限定名称。这不会影响 Kotlin 客户端查看此文件中声明的方式,但 Java 客户端和其他 JVM 语言客户端将看到类文件,就好像它是在指定包中声明的一样。如果一个文件被这个注解注解,它只能有函数、属性和类型别名声明,而不能有类。

@JvmSynthetic

在 Java 字节码中的注解目标上设置 ACC_SYNTHETIC 标志。

@JvmSuppressWildcards

指示编译器为类型参数生成或省略通配符,这些类型参数对应于具有声明站点差异的参数,例如 Collectionhas。如果最里面应用的@JvmSuppressWildcards 有suppress=true,则生成的类型不带通配符。如果最里面应用的@JvmSuppressWildcards 有suppress=false,则使用通配符生成类型。仅当声明似乎不便于从 Java 中使用时,它才可能有用。

@JvmWildcard

指示编译器为与具有声明站点差异的参数对应的带注释类型参数生成通配符。仅当在没有通配符的情况下从 Java 中使用声明似乎不方便时,它才可能会有所帮助。


原文始发于微信公众号(八千里路山与海):Kotlin Jvm Annotations

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/85047.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!