在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
用代码来表示,具体可能出现的位置以下图所示: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;
}
}
复制代码
对应上述其它三个位置的位置注解分别是:
除此以外,Kotlin还提供了如下几个位置注解,对应其它不一样使用场景:
这个注解用在文件级别,每个Kotlin文件对应一个或多个Java类,当对应一个类的时候,可经过添加该位置注解,结合上一节课讲到的注解@JvmName
一块儿使用,可改变生成的Java类名。
你能够理解为这个注解实际做用的位置就是最终编译生成的Java类。
@file:JvmName("FooKt")
fun foo() {
println("Hello, world...")
}
复制代码
最终生成的代码相似下面这样,生成的类名刚好是注解上方所填写的名称:
@JvmName("FooKt")
public final class FooKt {
public final void foo() {
...
}
}
复制代码
这个注解的做用是使注解出现的位置定位到构造函数的参数上面。
你们知道,在Kotlin语言中,若是在构造函数参数前面添加var
或val
关键词,在对应类中会生成相应的属性、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;
}
}
复制代码
这是一个特殊的位置注解,这个注解对于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没法访问并不许确。
这也是一个很是特殊的位置注解,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
位置注解的做用,理解了扩展函数的原理,这个注解的做用就不难理解了。
这是今天咱们要说的最后一个位置注解,这又是一个相对比较难理解的位置注解,由于在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