理解Kotlin语言独有的位置注解,让注解控制更精准

在Kotlin语言编写的代码中,你应该看到过相似这样的注解@file:JvmName(...),这有点难以理解,正常的注解不会存在相似@file:这样的前缀,在Java语言中也没有相似的语法。那么,这到底有什么做用呢? 因为其特殊的做用,我把它称之为”位置注解“。编程

Kotlin语言是一门将语法简化到极致的编程语言,咱们一块儿来看一段简单的代码:bash

class Person {
    var name: String? = null
}
复制代码

这段极其简单的代码,通过Kotlin编译器的处理,等价于下面这段Java代码:微信

public final class Person {
   @Nullable
   private String name;

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

   public final void setName(@Nullable String var1) {
      this.name = var1;
   }
}
复制代码

虽然在Kotlin语言中,看起来只是声明了一个成员变量,实际上编译后不只声明了一个成员变量name,还生成了与之对应的setter/getter方法。编程语言

这个时候,问题来了,若是咱们在Person类的name属性上方添加一个注解,会出现什么问题呢?函数

class Person {
   @Callable
   var name: String? = null
}
复制代码

咱们刚才说到,实际生成的字节码中包含了setter/getter方法,那么这个注解可能出现的位置就有4个地方:ui

  • 属性(成员变量name)
  • setter方法
  • setter方法参数name
  • getter方法

用代码来表示,具体可能出现的位置以下图所示:this

public final class Person {
   // 位置一:属性
   @Callable
   @Nullable
   private String name;
	
   // 位置二:setter方法
   @Callable
   public final void setName(/*位置三:setter方法参数*/ @Nullable String var1) {
      this.name = var1;
   }
   
   // 位置四:getter方法
   @Callable
   @Nullable
   public final String getName() {
      return this.name;
   }
}
复制代码

这个时候编译器晕菜了,它没法肯定你到底想要让注解出如今什么位置。那么,这种状况下,Kotlin编译器究竟会怎么作呢?感兴趣的同窗不妨本身作作实验。spa

那么,是否有办法使注解准确地出如今指定位置呢?答案是:固然有!位置注解刚好就是用来解决这个问题的。代理

咱们将上面的代码添加位置注解,修改成下面这样:code

class Person {
    @field:Callable
    var name: String? = null
}
复制代码

经过添加位置注解@field@Callable注解将准确出如今属性定义的位置,以下所示:

public final class Person {
   // 注解将出如今这里
   @Callable
   @Nullable
   private String name;
	
   public final void setName(@Nullable String var1) {
      this.name = var1;
   }
   
   @Nullable
   public final String getName() {
      return this.name;
   }
}
复制代码

对应上述其它三个位置的位置注解分别是:

  • set: 对应setter方法位置
  • get:对应getter方法位置
  • setparam:对应setter方法参数位置

除此以外,Kotlin还提供了如下几个位置注解,对应其它不一样使用场景:

@file:

这个注解用在文件级别,每个Kotlin文件对应一个或多个Java类,当对应一个类的时候,可经过添加该位置注解,结合上一节课讲到的注解@JvmName一块儿使用,可改变生成的Java类名。

你能够理解为这个注解实际做用的位置就是最终编译生成的Java类。

@file:JvmName("FooKt")
fun foo() {
    println("Hello, world...")
}
复制代码

最终生成的代码相似下面这样,生成的类名刚好是注解上方所填写的名称:

@JvmName("FooKt")
public final class FooKt {
	public final void foo() {
		...
	}
}
复制代码

@param:

这个注解的做用是使注解出现的位置定位到构造函数的参数上面。

你们知道,在Kotlin语言中,若是在构造函数参数前面添加varval关键词,在对应类中会生成相应的属性、setter、getter方法。

为了让注解准确地出如今其构造函数参数的位置,这个注解就应运而生了!

咱们继续来看一个例子:

class Person(@param:Callable var name: String)
复制代码

添加上述位置注解后,最终生成的注解就会出如今构造函数参数的位置,以下所示:

public final class Person {
   @NotNull
   private String name;

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

   public final void setName(@NotNull String var1) {
      this.name = var1;
   }
	
   // 注解最终出如今了这里
   public Person(@Callable @NotNull String name) {
      super();
      this.name = name;
   }
}
复制代码

@property:

这是一个特殊的位置注解,这个注解对于Java端是不可见的,其表明的位置是对应属性的Property对象。这样提及来有点抽象,咱们来看一个例子,先来了解一下Property究竟是什么东西。

咱们继续以Person类为例,经过下面一段代码去访问它:

fun main(args: Array<String>) {
    val person = Person("Scott")
    
    val propertyName = person::name
    // 这里将打印 name: falsee
    println("${propertyName.name}: ${propertyName.isConst}")
}
复制代码

上述代码中,propertyName对应的就是Person类中name属性的Property实例,简单来讲就是,Property保存了对应属性的相关信息,表明了当前属性。经过Property能够获取到当前属性的相关信息(包括变量的名称,是否常量,是否延迟初始化等等)。

若是在构造函数的name前面添加位置注解@property:,注解生成的位置会稍微有点难以理解。访问这个注解的惟一方法就是经过其Property实例,咱们一块儿来试一下:

class Person(@property:Callable var name: String)
复制代码

访问该注解的惟一方式是经过其Property实例,而且Java端没法访问到:

fun main(args: Array<String>) {
    val person = Person("Scott")

    val propertyName = person::name
    // 访问该注解的惟一方式
    println(propertyName.annotations.find { it.annotationClass == Callable::class })
}
复制代码

那么,具体到字节码,该注解到底出如今了哪里呢?咱们不妨来反编译看一看:

public final class Person {
   @NotNull
   private String name;
	
   // 注解出如今了这里,很是特殊的一个位置
   @Callable
   public static void name$annotations() {
   }

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

   public final void setName(@NotNull String var1) {
      this.name = var1;
   }

   public Person(@NotNull String name) {
      super();
      this.name = name;
   }
}
复制代码

能够看到注解出如今了Kotlin编译器生成的一个以属性名称加**$annotations**后缀做为方法命名的静态方法上。这是Kotlin编译器约定的一个特殊方法,经过Property实例能够准确访问到这里。

而知道了这个约定命名方式以后,事实上Java端也能够经过特殊的方式来访问到该注解,严格来说,Java没法访问并不许确。

@receiver:

这也是一个很是特殊的位置注解,Kotlin支持扩展函数,即在不经过继承的状况下对原有类扩展函数或属性。扩展中有一个很重要的概念就是receiver,所谓的receiver,就是指被扩展类的实例自己。

但问题来了,扩展并不会改变原有类的代码,如何将注解放到receiver位置呢,这彷佛是一个不可能完成的事情。

这就要说到扩展的实现原理了,扩展实际上对应Kotlin中的一个全局函数,当转换到字节码的时候,函数的第一个参数就是receiver自己。这样提及来可能比较抽象,咱们直接来看一个例子:

咱们先对Person类增长扩展函数sayHi:

fun Person.sayHi(greet: String) {
    println("$greet, $name")
}
复制代码

而后反编译查看最终获得的Java代码:

public static final void sayHi(@NotNull Person $receiver, @NotNull String greet) {
  String var2 = greet + ", " + $receiver.getName();
  System.out.println(var2);
}
复制代码

能够看到Kotlin编译器生成了一个静态方法,静态方法的第一个参数就是receiver,对应扩展类实例自己,第二个参数是扩展函数实际的参数。

这就是Kotlin扩展的实现原理,其最终是经过增长静态函数来实现的,扩展函数的第一个参数永远指向被扩展类的实例,即receiver。而咱们添加了位置注解@receiver以后,注解生成的位置就会出如今扩展函数第一个参数的位置,相似下面这样:

public static final void sayHi(@Callable @NotNull Person $receiver, @NotNull String greet) {
  String var2 = greet + ", " + $receiver.getName();
  System.out.println(var2);
}
复制代码

这就是@receiver位置注解的做用,理解了扩展函数的原理,这个注解的做用就不难理解了。

@delegate

这是今天咱们要说的最后一个位置注解,这又是一个相对比较难理解的位置注解,由于在Java语言中并不存在相似的概念。在Kotlin语言中代理模式大行其道,Kotlin语言使用by关键字就能够轻松实现代理模式。

这里存在一个一样的问题,前面咱们说过,在Kotlin类中声明一个属性实际会同时生成setter/getter方法,这样注解可能出现的位置除属性以外就是三处(setter/getter/setter参数)。而若是属性自己使用代理的方式生成,这里就多了一个位置:代理类属性的位置。

这样说,可能还不太直观,咱们用官方的lazy实现来举一个例子。

咱们在Person类中增长一个代理属性gender

class Person(var name: String) {
    @delegate:Callable
    val gender by lazy { "male" }
}
复制代码

老规矩,咱们仍是直接反编译获得Java代码再来分析:

public final class Person {
   // 注解出如今了这个位置
   // 也就是真正的代理类实例的位置
   @Callable
   @NotNull
   private final Lazy gender$delegate;

   @NotNull
   public final String getGender() {
      Lazy var1 = this.gender$delegate;
      return (String)var1.getValue();
   }

   public Person(@NotNull String name) {
      super();
      this.name = name;
      this.gender$delegate = LazyKt.lazy((Function0)null.INSTANCE);
   }
}
复制代码

经过上面的代码,咱们能够清晰地看到Kotlin编译器在类中生成真正的代理类实例属性,gender的值实际是从代理对象中获取的。这个位置注解的做用就是将注解精确地放置到代理类的实例属性上方。

默认位置注解优先级

位置注解在Kotlin语言中并非强制要求的,咱们能够不添加位置注解,在未添加位置注解的状况下,Kotlin语言会按照下面的优先级将注解放置到指定的位置(若是注解能够同时出如今多个位置的话):

param > property > field

最佳实践

以上就是Kotlin语言中咱们能够用到的全部位置注解,这是由于Kotlin语言将语法简化到了极致,咱们才须要这些注解精确地告诉编译器须要将注解放置到哪里。若是你须要在代码中添加注解,应该始终记得增长位置注解,以便注解能够精确地放置到你想要放置的位置,避免出现一些没必要要的麻烦。

阅读更多技术文章,请关注微信公众号”欧阳锋工做室“

参与Kotlin技术讨论,请添加惟一官方QQ交流群:329673958

相关文章
相关标签/搜索