跳到主要内容

18

Kotlin 中的预置注解

在 Kotlin 中最大的一个特点就是可以和 Java 做到极高的互操作性,我们知道 Kotlin 的语法和 Java 语法还是有很大的不同,要想做到与 Java 做到很大兼容性可能需要携带一些额外信息,供编译器或者运行时做类似兼容转换。其中注解就起到了很大的作用,在 Kotlin 内置很多个注解为了解决 Java 中的调用 Kotlin API 的一些调用习惯和控制 API 的调用。它们就是 Kotlin 中的 @Jvm 系列的注解,咱们一一来看下它们都有哪些。

1. @JvmDefault

Tips:@JvmDefault 注解是在 Kotlin 1.2.40 版本加入的,并且在之后的 Kotlin 1.2.50 版本增强一些实验性特性。

@JvmDefault 的作用

我们知道在 Kotlin 中接口中可以增加非抽象成员,那么该注解就是为非抽象的接口成员生成默认的方法

使用-Xjvm-default = enable,会为每个@JvmDefault注解标注的方法生成接口中的默认方法。在此模式下,使用@JvmDefault注解现有方法可能会破坏二进制兼容性,因为它将有效地从DefaultImpls类中删除该方法。

使用-Xjvm-default = compatibility,除了默认接口方法之外,还会生成兼容性访问器。在DefaultImpls类中,它通过合成访问器调用默认接口方法。在此模式下,使用@JvmDefault注解现有方法是二进制兼容的,但在字节码中会产生更多方法。从接口成员中移除此注解会使在两种模式中的二进制不兼容性发生变化。

源码定义

@SinceKotlin("1.2")//从Kotlin的1.2版本第一次出现该注解
@RequireKotlin("1.2.40", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)//目标对象是函数和属性
annotation class JvmDefault

使用注解前后反编译 Java 代码对比

未使用 @JvmDefault 注解:

interface ITeaching {
fun speak() = println("open the book")
}

class ChineseTeacher : ITeaching

fun main(args: Array<String>) {
ChineseTeacher().speak()
}

反编译成 Java 代码:

public interface ITeaching {
void speak();
public static final class DefaultImpls {//可以看到在接口为speak函数生成一个DefaultImpls静态内部类
public static void speak(ITeaching $this) {
String var1 = "open the book";
System.out.println(var1);
}
}
}

public final class ChineseTeacher implements ITeaching {
public void speak() {
ITeaching.DefaultImpls.speak(this);//注意:这里却是直接调用ITeaching中静态内部类DefaultImpls的speak方法。
}
}

public final class JvmDefaultTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
(new ChineseTeacher()).speak();//这里会调用ChineseTeacher中的speak方法
}
}

使用 @JvmDefault 注解

interface ITeaching {
@JvmDefault//注意: 可能一开始使用该注解会报错,需要在gradle中配置jvm参数:-jvm-target=1.8 -Xjvm-default=enable
fun speak() = println("open the book")
}

class ChineseTeacher : ITeaching

fun main(args: Array<String>) {
ChineseTeacher().speak()
}

反编译成 Java 代码

public interface ITeaching {
@JvmDefault
default void speak() {//添加注解后外层的静态内部类被消除
String var1 = "open the book";
System.out.println(var1);
}
}

public final class ChineseTeacher implements ITeaching {//内部并没有类似上面的speak方法调用的桥接委托
}

public final class JvmDefaultTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
(new ChineseTeacher()).speak();
}
}

总而言之,在没有添加 **@JvmDefault **注解,Kotlin 会自动生成一个叫做 DefaultImpl 静态内部类,用于保存静态方法的默认实现,并使用自身接收器类型来模拟属于对象的方法

然后,对于扩展该接口的每种类型,如果类型没有实现方法本身,则在编译时,Kotlin将通过调用将方法连接到默认实现。

这样一来确实带来一个很大好处就是在 JDK1.8 之前的版本JVM上提供了在接口上也能定义具体的实现方法功能。但是这样也存在一些问题:

  • 第一问题:比如它和现在的 Java 的处理方式不兼容,这样会导致互操作性极度下降。我们甚至可以在Java中直接去调用自动生成的DefaultImpls,类似这样的调用:ITeaching.DefaultImpls.speak(new ChineseTeacher());,这样内部的细节居然也能暴露给外部,这样更会让调用者一脸懵逼。
  • 第二问题:Java 8 中存在默认方法的主要原因之一是能够向接口添加方法而无需侵入每个子类。 然而 Kotlin 实现不支持这个原因是必须在每个具体类型上生成默认调用。 向接口添加新方法导致必须重新编译每个实现者。

基于上述问题,Kotlin 推出了 **@JvmDefault **注解

2. @JvmField

@JvmField 作用

可以应用于一个字段,把这个属性暴露成一个没有访问器的公有 Java 字段以及 Companion Object 对象中。

源码定义

@Target(AnnotationTarget.FIELD)//作用对象是字段,包括属性的幕后字段
@Retention(AnnotationRetention.BINARY)//注解保留期是源码阶段
@MustBeDocumented
public actual annotation class JvmField

@JvmField 注解使用

使用场景一:我们知道在 Kotlin 中默认情况下,Kotlin 类不会公开字段而是会公开属性。Kotlin 会为属性的提供幕后字段,这些属性将会以字段形式存储它的值。一起来看个例子:

//Person类中定义一个age属性,age属性默认是public公开的,但是反编译成Java代码,你就会看到它的幕后字段了。
class Person {
var age = 18
set(value) {
if (value > 0) field = value
}
}

反编译成 Java 代码:

public final class Person {
private int age = 18;//这个就是Person类中的幕后字段,可以看到age字段是private私有的。

//外部访问通过setter和getter访问器来操作。由于Kotlin自动生成setter、getter访问器,所以外部可以直接类似公开属性操作,
//实际上内部还是通过setter、getter访问器来实现
public final int getAge() {
return this.age;
}

public final void setAge(int value) {
if (value > 0) {
this.age = value;
}
}
}

但是如果在 Kotlin 需要生成一个公开的字段怎么实现呢?那就要借助@JvmField注解了,它会自动将该字段的 setter、getter 访问器消除掉,并且把这个字段修改为 public。

class Person {
@JvmField
var age = 18
}

反编译成的 Java 代码

public final class Person {
@JvmField
public int age = 18;//消除了setter、getter访问器,并且age字段为public公开
}

使用场景二@JvmField另一个经常使用的场景就是用于Companion Object伴生对象中。

未使用@JvmField注解:

class Person {
companion object {
val MAX_AGE = 120
}
}

反编译成 Java 代码

public final class Person {
private static final int MAX_AGE = 120;//注意: 这里默认是private私有的MAX\_AGE,所以在Java中调用无法直接通过Person类名.变量名访问
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public static final class Companion {
//在Java中调用无法直接通过Person类名.变量名访问,
//而是通过静态内部类Companion的getMAX\_AGE间接访问,类似这样Person.Companion.getMAX\_AGE();
public final int getMAX\_AGE() {
return Person.MAX_AGE;
}

private Companion() {
}

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

但是如果使用该注解就能直接通过 Person 类名.变量名 访问:

class Person {
companion object {
@JvmField
val MAX_AGE = 120
}
}
//在Java中调用
public static void main(String[] args) {
System.out.println(Person.MAX_AGE);//可以直接调用,因为它已经变成了public了
}

反编译成 Java 代码

public final class Person {
@JvmField
public static final int MAX_AGE = 120;//公有的MAX\_AGE的,外部可以直接调用
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}

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

3. @JvmMultifileClass

@JvmMultifileClass 的作用

该注解主要是为了生成多文件的类。

源码定义

@Target(AnnotationTarget.FILE)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmMultifileClass()

注解使用

在 Kotlin 分别定义两个顶层函数在两个不同文件中,可通过该注解将多个文件中的类方法合并到一个类中。

//存在于IOUtilA文件中
@file:JvmName("IOUtils")
@file:JvmMultifileClass

package com.mikyou.annotation

import java.io.IOException
import java.io.Reader


fun closeReaderQuietly(input: Reader?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}

//存在于IOUtilB文件中
@file:JvmName("IOUtils")
@file:JvmMultifileClass

package com.mikyou.annotation

import java.io.IOException
import java.io.InputStream


fun closeStreamQuietly(input: InputStream?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}
//在Java中使用
public class Test {
public static void main(String[] args) {
//即使存在于不同文件中,但是对于外部Java调用仍然是同一个类IOUtils
IOUtils.closeReaderQuietly(null);
IOUtils.closeStreamQuietly(null);
}
}

4. @JvmName

@JvmName 的作用

将改变由 Kotlin 默认生成的 Java 方法、字段或类名。

源码定义

//作用的目标有: 函数、属性getter方法、属性setter方法、文件
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FILE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmName(actual val name: String)//有个name参数,将生成传入指定name的名称

注解使用

class Student {
@get:JvmName(name = "getStudentName")//修改属性的getter函数名称
@set:JvmName(name = "setStudentName")//修改属性的setter函数名称
var name: String = "Tim"

@JvmName("getStudentScore")//修改函数名称
fun getScore(): Double {
return 110.5
}
}
//修改生成的类名,默认Kotlin会生成以文件名+Kt后缀组合而成的类名
@file:JvmName("IOUtils")//注意:该注解一定要在第一行,package顶部
package com.mikyou.annotation

import java.io.IOException
import java.io.Reader


fun closeReaderQuietly(input: Reader?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}

反编译后的 Java 代码

public final class Student {
@NotNull
private String name = "Tim";

@JvmName(name = "getStudentName")
@NotNull
//已经修改成传入getStudentName
public final String getStudentName() {
return this.name;
}

@JvmName(name = "setStudentName")
//已经修改成传入setStudentName
public final void setStudentName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name = var1;
}

@JvmName(name = "getStudentScore")
//已经修改成传入getStudentScore
public final double getStudentScore() {
return 110.5D;
}
}

5. @JvmOverloads

@JvmOverloads 的作用

指导 Kotlin 编译器为带默认参数值的函数(包括构造函数)生成多个重载函数。

源码定义

//作用对象是函数和构造函数
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmOverloads()

注解使用

该注解使用最多就是用于带默认值函数的重载,在 Android 中我们在自定义 View 的时候一般会重载多个构造器,需要加入该注解,如果不加默认只定义一个构造器,那么当你直接在 XML 使用这个自定义 View 的时候会抛出异常。

class ScrollerView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attr, defStyle) {
//...
}

反编译后的 Java 代码

public final class ScrollerView extends View {
@JvmOverloads
public ScrollerView(@NotNull Context context, @Nullable AttributeSet attr, int defStyle) {
Intrinsics.checkParameterIsNotNull(context, "context");
super(context, attr, defStyle);
}

// $FF: synthetic method
@JvmOverloads
public ScrollerView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 2) != 0) {
var2 = (AttributeSet)null;
}
if ((var4 & 4) != 0) {
var3 = 0;
}
this(var1, var2, var3);
}

@JvmOverloads
public ScrollerView(@NotNull Context context, @Nullable AttributeSet attr) {
this(context, attr, 0, 4, (DefaultConstructorMarker)null);
}

@JvmOverloads
public ScrollerView(@NotNull Context context) {
this(context, (AttributeSet)null, 0, 6, (DefaultConstructorMarker)null);
}
//...
}

6. @JvmPackageName

@JvmPackageName 的作用

更改从使用该注解标注的文件生成的 .class 文件的 JVM 包的完全限定名称。 这不会影响 Kotlin 客户端在此文件中查看声明的方式,但 Java 客户端和其他 JVM 语言客户端将看到类文件,就好像它是在指定的包中声明的那样。 如果使用此批注对文件进行批注,则它只能包含函数,属性和类型声明,但不能包含

源码定义

@Target(AnnotationTarget.FILE)//作用于文件
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
@SinceKotlin("1.2")//Kotlin1.2版本加入
internal annotation class JvmPackageName(val name: String)

注解使用

//以Collection源码为例
@file:kotlin.jvm.JvmPackageName("kotlin.collections.jdk8")

package kotlin.collections

可以看到该类会编译生成到kotlin.collections.jdk8包名下

图片描述

7. @JvmStatic

@JvmStatic 的作用

能被用在对象声明或者 Companion object 伴生对象的方法上,把它们暴露成一个 Java 的静态方法

源码定义

//作用于函数、属性、属性的setter和getter
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmStatic()

注解使用

@JvmStatic 这个注解一般经常用于伴生对象的方法上,供给 Java 代码调用:

class Data {
companion object {
fun getDefaultDataName(): String {
return "default"
}
}
}
//在java中调用,只能是Data.Companion.getDefaultDataName()调用
public class Test {
public static void main(String[] args) {
System.out.println(Data.Companion.getDefaultDataName());
}
}

反编译后 Java 代码:

public final class Data {
public static final Data.Companion Companion = new Data.Companion((DefaultConstructorMarker)null);
public static final class Companion {
@NotNull
public final String getDefaultDataName() {
return "default";
}

private Companion() {
}

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

使用 @JvmStatic 注解后:

class Data {
companion object {
@JvmStatic
fun getDefaultDataName(): String {
return "default"
}
}
}
//在java中调用,可以直接这样Data.getDefaultDataName()调用
public class Test {
public static void main(String[] args) {
System.out.println(Data.getDefaultDataName());
}
}

反编译后的 Java 代码

public final class Data {
public static final Data.Companion Companion = new Data.Companion((DefaultConstructorMarker)null);

@JvmStatic
@NotNull
//注意它会在Data类内部自动生成一个getDefaultDataName,然后内部还是通过Companion.getDefaultDataName()去调用。
public static final String getDefaultDataName() {
return Companion.getDefaultDataName();
}

public static final class Companion {
@JvmStatic
@NotNull
public final String getDefaultDataName() {
return "default";
}

private Companion() {
}

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

8. @JvmSuppressWildcards和@JvmWildcard

@JvmSuppressWildcards 和 @JvmWildcard 的作用

用于指示编译器生成或省略类型参数的通配符,JvmSuppressWildcards用于参数的泛型是否生成或省略通配符,而JvmWildcard用于返回值的类型是否生成或省略通配符

源码定义

//作用于类、函数、属性、类型
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@MustBeDocumented
@OptionalExpectation
//指定suppress为true表示不生成,false为生成通配符,默认是true不生成
public expect annotation class JvmSuppressWildcards(val suppress: Boolean = true)

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmWildcard

注解使用

interface ICovert {
fun covertData(datas: List<@JvmSuppressWildcards(suppress = false) String>)//@JvmSuppressWildcardsd用于参数类型

fun getData(): List<@JvmWildcard String>//@JvmWildcard用于返回值类型
}

class CovertImpl implements ICovert {
@Override
public void covertData(List<? extends String> datas) {//参数类型生成通配符

}
@Override
public List<? extends String> getData() {//返回值类型生成通配符
return null;
}
}

9. @JvmSynthetic

@JvmSynthetic 的作用

它在生成的类文件中将适当的元素标记为合成,并且编译器标记为合成的任何元素都将无法从 Java 语言中访问。

什么是合成属性(Synthetic属性)?

JVM 字节码标识的 ACC_SYNTHETIC 属性用于标识该元素实际上不存在于原始源代码中,而由编译器生成。

合成属性能做什么?

它一般用于支持代码生成,允许编译器生成不应向其他开发人员公开但需要支持实际公开接口所需的字段和方法。我们可以将其视为超越 private 或 protected 级别。

源码定义

//作用于函数、属性的setter,getter以及字段
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD)
@OptionalExpectation
public expect annotation class JvmSynthetic()

注解使用

class Synthetic {
@JvmSynthetic
val name: String = "Tim"
var age: Int
@JvmSynthetic
set(value) {
}
@JvmSynthetic
get() {
return 18
}
}

反编译后的 Java 代码

public final class Synthetic {
// $FF: synthetic field
@NotNull
private final String name = "Tim";

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

// $FF: synthetic method//我们经常看到这些注释,就是通过@Synthetic注解生成的
public final int getAge() {
return 18;
}

// $FF: synthetic method
public final void setAge(int value) {
}
}

通过反编译代码可能看不到什么,我们直接可以通过 javap -v xxx.class 查阅生成的字节码文件描述:

  public final int getAge();
descriptor: ()I
flags: ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC//添加ACC\_SYNTHETIC标识
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/mikyou/annotation/Synthetic;
LineNumberTable:
line 12: 0

public final void setAge(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC//添加ACC\_SYNTHETIC标识
Code:
stack=0, locals=2, args_size=2
0: return
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/mikyou/annotation/Synthetic;
0 1 1 value I
LineNumberTable:
line 9: 0


10. @Throws

@Throws 作用

用于Kotlin中的函数,属性的 setter 或 getter 函数,构造器函数抛出异常。

源码定义

//作用于函数、属性的getter、setter函数、构造器函数
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
public annotation class Throws(vararg val exceptionClasses: KClass<out Throwable>)//这里是异常类KClass不定参数,可以同时指定一个或多个异常

注解使用

@Throws(IOException::class)
fun closeQuietly(output: Writer?) {
output?.close()
}

11. Kotlin 中的其他预置注解

Kotlin 中还有一些其他的预置注解,在这里就不再一一演示了,知道它们的作用即可。

  • @Transient:该注解充当了 Java 中的 transient 关键字;
  • @Strictfp:该注解充当了 Java 中的 strictfp 关键字;
  • @Synchronized:该注解充当了 Java 中的 synchronized 关键字;
  • @Volatile:该注解充当了 Java 中的 volatile 关键字。