在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