大多數(shù)情況下,你不需要關注這個問題。但是,如果你的代碼中包含了部分Java代碼,理解這些注解將幫助你解決很多棘手問題。
產(chǎn)生這個問題的根本原因在于:Kotlin語言與Java語言的設計思路不同,部分特性屬于Java語言獨有,例如靜態(tài)變量。部分特性屬于Kotlin語言獨有,例如逆變和協(xié)變。
為了抹平這些差異,Kotlin語言提供了一個絕佳的思路,通過添加注解可以改變Kotlin編譯器生成的Java字節(jié)碼,使之按照Java語言可以理解的方向進行,從而實現(xiàn)兼容。
問題答疑:Kotlin語言與Java字節(jié)碼有什么關系?為什么Kotlin編譯器會生成Java字節(jié)碼?
不管是Kotlin語言還是Java語言都是建立在JVM平臺上面的編程語言,其最終都需要編譯成JVM可以識別的Java字節(jié)碼才能被正確執(zhí)行。這也是為什么Kotlin語言與Java可以完全互通的原因之一,不要將Java與Java平臺混為一談。
接下來我們先來看第一個注解,也是最常用到的一個注解:
@JvmField
Kotlin編譯器默認會將類中聲明的成員變量編譯成私有變量,Java語言要訪問該變量必須通過其生成的getter方法。而使用上面的注解可以向Java暴露該變量,即使其訪問變?yōu)楣_(修飾符變?yōu)閜ublic)。
我們來做一個實驗:
1)新建Person.kt,添加如下代碼:
class Person {
@JvmField
var name: String? = null
}
2)新建Client.java,添加如下代碼,嘗試訪問Person類中的變量name:
public class Client {
public static void main(String[] args) {
Person p = new Person();
// 在添加@JvmField注解之前,這樣訪問會報錯
// 只能通過p.getName()的方式進行訪問
String name = p.name;
}
}
在添加@JvmField屬性前我們試圖通過p.name的方式進行訪問,編譯器出現(xiàn)報錯。因為,默認生成的成員變量name是私有的。而添加該注解之后我們居然可以正常訪問了。
由此可見,@JvmField注解的確使生成的字節(jié)碼發(fā)生了變化,我們將字節(jié)碼用Java代碼來表示,具體發(fā)生的變化類似下面代碼發(fā)生的變化:
添加注解之前
public final class Person {
private String name;
public final String getName() {
return this.name;
}
public final void setName(@Nullable String var1) {
this.name = var1;
}
}
添加注解之后
public final class Person {
public String name;
}
以上場景是將@JvmField注解添加到普通變量上方,如果添加到伴隨對象的成員變量上方,會發(fā)生什么呢?我們來試試看:
class Person {
var name: String? = null
companion object {
@JvmField
val GENDER_MALE = 1
}
}
public static void main(String[] args) {
// 未添加之前
// int gender = Person.Companion.getGENDER_MALE();
// 添加之后,可直接訪問
int gender = Person.GENDER_MALE;
System.out.println(gender);
}
同樣地,添加注解之后我們可以通過點語法直接對其進行訪問。
由此可見,@JvmField注解會使伴隨對象在伴生類中生成靜態(tài)成員變量,通過伴生類可直接對其進行訪問。
結論
@JvmField注解可改變字節(jié)碼的生成,其作用的目標是類成員變量或伴隨對象成員變量。作用在類成員中可使該變量對外暴露,通過點語法直接訪問。即將私有成員變量公有化(public),并去掉setter/getter方法。作用在伴隨對象成員變量中,可以使該伴隨對象中的變量生成在伴生對象中,成為伴生對象的公有靜態(tài)成員變量,通過伴生類可直接訪問。
那么問題來了,如果該注解作用在私有成員變量上方會發(fā)生什么呢?請大家自行驗證。
@JvmStatic
這個注解與@JvmField非常容易出現(xiàn)混淆,兩者都可以作用在伴隨對象成員變量上方,我們來試試看,如果同樣作用在伴隨對象成員變量中,會出現(xiàn)什么情況。
添加@JvmField注解的效果,上面我們已經(jīng)看到了,我們直接將注解修改為@JvmStatic試試看:
class Person {
var name: String? = null
companion object {
@JvmStatic
val GENDER_MALE = 1
}
}
public static void main(String[] args) {
// 1) 這樣訪問報錯
int gender = Person.GENDER_MALE;
// 2) 這樣訪問正常
int gender = Person.Companion.getGENDER_MALE();
// 3) 這樣訪問也正常
int gender = Person.getGENDER_MALE();
System.out.println(gender);
}
切換到Java代碼,你可以看到,我一共提供了三種訪問方式。第一種訪問方式是通過點語法直接訪問,編譯器報錯,由此可見,@JvmStatic注解并沒有在伴生類中生成靜態(tài)的公有成員變量。第三種方式可以正常訪問,證明該注解在伴生類中生成了靜態(tài)的公有getter方法。第二種方式可以正常訪問,證明該注解不會破壞伴隨對象中原有成員的訪問方式。
由此,我們可以大膽猜測,@JvmStatic注解的作用應該是生成靜態(tài)的setter/getter方法,而不會改變屬性(成員變量)的訪問權限。
為了進一步驗證我們的猜想,我們將val修改為var試試看。
public static void main(String[] args) {
// 1) 這樣訪問報錯
int gender = Person.GENDER_MALE;
// 2) 這樣訪問正常
int gender = Person.Companion.getGENDER_MALE();
// 3) 這樣訪問也正常
int gender = Person.getGENDER_MALE();
// 4) 以下訪問正常
Person.setGENDER_MALE(1);
System.out.println(gender);
}
第四種方式調用正常,證明我們的猜測沒有錯,@JvmStatic僅會改變伴隨對象或對象(object)中setter/getter方法的生成方式,而不會改變屬性訪問權限,這是與注解@JvmField的本質區(qū)別。
注意:由于@JvmField不僅會改變屬性的訪問權限,同時也會改變setter/getter方法的生成,細心的同學應該已經(jīng)注意到了。一旦添加了@JvmField注解,setter/getter方法也消失了(變量可以通過點語法直接訪問,setter/getter方法也就沒必要存在了)。而@JvmStatic僅僅是使setter/getter方法變?yōu)殪o態(tài)方法,同時生成位置放置到伴生類中。這與@JvmField的處理方式有些沖突(@JvmField會直接刪除掉setter/getter方法)。為了避免沖突,Kotlin語言禁止將這兩個注解混淆使用。
以上是將@JvmStatic與@JvmField作用在伴隨對象成員變量上的區(qū)別。實際上,@JvmStatic不僅可以修飾屬性(成員變量),還可以修飾方法,修飾方法的作用與修飾屬性的作用一致,都是將方法變成靜態(tài)類型。
為了更直觀地表示兩種的區(qū)別,我們用一個表格完整展示兩個注解的區(qū)別:
| 注解 | 作用位置 | 作用 |
|---|---|---|
@JvmField |
類屬性或對象屬性 | 使屬性修飾符成為public |
@JvmStatic |
對象方法(包括伴生對象) | 使用方法成為靜態(tài)類型,如果作用在伴生對象方法中,其方法會成為伴生類的靜態(tài)方法 |
@JvmName
這個注解可以改變字節(jié)碼中生成的類名或方法名稱,如果作用在頂級作用域(文件中),則會改變生成對應Java類的名稱。如果作用在方法上,則會改變生成對應Java方法的名稱。
Test.kt
@file:JvmName("FooKt")
@JvmName("foo1")
fun foo() {
println("Hello, Jvm...")
}
在Kotlin語言中,foo是一個全局方法,為了兼容Java字節(jié)碼,實際會根據(jù)文件名生成對應的Java類TestKt.java,這是Kotlin編譯器的一個隱藏規(guī)則。
而添加了上述注解之后,生成的類名與方法名均發(fā)生了變化,具體產(chǎn)生的變化相當于下面這段Java代碼:
// 相當于下面的Java代碼
public final class FooKt {
public static final void foo1() {
String var0 = "Hello, Jvm...";
System.out.println(var0);
}
}
可以看到第一個注解@file:JvmName("FooKt")的作用是使生成的類名變?yōu)?code>FooKt,第二個注解的作用是使生成的方法名稱變?yōu)?code>foo1。
注意:該注解不能改變類中生成的屬性(成員變量)的名稱。
這里的注解中,我們看到了一個特殊的前綴@file:,這個注解前綴是Kotlin語言特有的一種標識,其作用是標記該注解最終會作用在生成的字節(jié)碼的具體位置(屬性、setter、getter等),關于這個部分,大家可以先跳過,下一篇文章將給大家詳細講解。
@JvmMultifileClass
說完了上面這個注解,就不得不提到@JvmMultifileClass這個注解,這個注解通常是與@JvmName結合使用的。其使用場景比較單一,看下面的例子:
新建文件Util1.kt,添加如下代碼:
@file:JvmName("Utils")
fun isEmpty(str: String?): Boolean {
return null == str || str.length <= 0
}
新建文件Util2.kt,添加如下代碼:
@file:JvmName("Utils")
fun isPhoneNumber(str: String): Boolean {
return str.startsWith("1") && str.length == 11
}
編譯以上代碼,Kotlin編譯器會提示錯誤Error:(1, 1) Kotlin: Duplicate JVM class name 'Utils' generated from: package-fragment, package-fragment,即生成的類名出現(xiàn)了重復??墒牵绻覀兙褪窍M暶魇褂枚鄠€文件,但方法生成到同一個類中呢?@JvmMultifileClass就是為解決這個問題而生的。
我們在上面代碼的基礎上分別添加注解@JvmMultifileClass試試看:
@file:JvmName("Utils")
@file:JvmMultifileClass
fun isEmpty(str: String?): Boolean {
return null == str || str.length <= 0
}
@file:JvmName("Utils")
@file:JvmMultifileClass
fun isPhoneNumber(str: String): Boolean {
return str.startsWith("1") && str.length == 11
}
添加注解@JvmMultifileClass之后,報錯消失了,反編譯生成的字節(jié)碼,我們發(fā)生兩個不同文件中的方法合并到了同一個類Utils中:
// 生成的代碼相當于下面這段Java代碼
public final class Utils {
public static final boolean isEmpty(@Nullable String str) {
return Utils__A1Kt.isEmpty(str);
}
public static final boolean isPhoneNumber(@NotNull String str) {
return Utils__A2Kt.isPhoneNumber(str);
}
}
這個注解在處理多個文件聲明,合并到一個類的場景中發(fā)揮著舉足輕重的作用。如果你有這樣的需求,一定要謹記這個注解。
@JvmOverloads
由于Kotlin語言支持方法參數(shù)默認值,而實現(xiàn)類似功能Java需要使用方法重載來實現(xiàn),這個注解就是為解決這個問題而生的,添加這個注解會自動生成重載方法。我們來試一下:
@JvmOverloads
fun foo(x: Int, y: Int = 0, z: Int = 0): Int {
return x + y + z
}
// 生成的代碼相當于下面這段Java代碼
public static final int foo(int x, int y, int z) {
return x + y + z;
}
public static final int foo(int x, int y) {
return foo(x, y, 0);
}
public static final int foo(int x) {
return foo(x, 0, 0);
}
由此可見,通過這個注解可以影響帶有參數(shù)默認值方法的生成,添加該注解將自動生成帶有默認值參數(shù)數(shù)量的重載方法。這是一個非常有用的特性,方便Java端可以更高效地調用Kotlin端代碼。
@Throws
由于Kotlin語言不支持CE(Checked Exception),所謂CE,即方法可能拋出的異常是已知的。Java語言通過throws關鍵字在方法上聲明CE。為了兼容這種寫法,Kotlin語言新增了@Throws注解,該注解的接收一個可變參數(shù),參數(shù)類型是多個異常的KClass實例。Kotlin編譯器通過讀取注解參數(shù),在生成的字節(jié)碼中自動添加CE聲明。
為了便于理解,看一個簡單的例子:
@Throws(IllegalArgumentException::class)
fun div(x: Int, y: Int): Float {
return x.toFloat() / y
}
// 生成的代碼相當于下面這段Java代碼
public static final float div(int x, int y) throws IllegalArgumentException {
return (float)x / (float)y;
}
可以看到,添加了@Throws(IllegalArgumentException::class)注解后,在生成的方法簽名上自動添加了可能拋出的異常聲明(throws IllegalArgumentException),即CE。
這個注解在保證邏輯的嚴謹性方面非常有用,但如果你的工程中僅使用Kotlin代碼,可以不用理會該注解。在Kotlin語言的設計哲學里面,CE被認為是一個錯誤的設計。
@Synchronized
這個注解很容易理解,顧名思義,主要用于產(chǎn)生同步方法。Kotlin語言不支持synchronized關鍵字,處理類似Java語言的并發(fā)問題,Kotlin語言建議使用同步方法進行處理。
Kotlin團隊認為同步的邏輯應該交給代碼處理,而不應該在語言層面處理:
但為了兼容Java,Kotlin語言支持使用該注解讓編譯器自動生成同步方法:
@Synchronized
fun start() {
println("Start do something...")
}
// 生成的代碼相當于下面這段Java代碼
public static final synchronized void start() {
String var0 = "Start do something...";
System.out.println(var0);
}
@JvmWildcard
這個注解主要用于處理泛型參數(shù),這涉及到兩個新的知識點:逆變與協(xié)變。由于Java語言不支持協(xié)變,為了保證安全地相互調用,可以通過在泛型參數(shù)聲明的位置添加該注解使用Kotlin編譯器生成通配符形式的泛型參數(shù)(?extends ...)。
看下面這段代碼:
class Box<out T>(val value: T)
interface Base
class Derived : Base
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
按照正常思維,下面的兩個方法轉換到Java代碼應該是這樣:
Box<Derived> boxDerived(Derived value) { …… }
Base unboxBase(Box<Base> box) { …… }
但問題是,Kotlin泛型支持型變,在Kotlin中,我們可以這樣寫unboxBase(Box(Derived())),而在Java語言中,泛型參數(shù)類型是不可變的,按照上面的寫法顯然已經(jīng)做不到了。
正確轉換到Java代碼應該是這樣:
Base unboxBase(Box<? extends Base> box) { …… }
為了使這樣的轉換正確生成,我們需要在泛型參數(shù)的位置添加上面的注解:
fun unboxBase(box: Box<@JvmWildcard Base>): Base = box.value
@JvmSuppressWildcards
這個注解的作用與@JvmWildcard恰恰相反,它是用來抑制通配符泛型參數(shù)的生成,即在不需要型變泛型參數(shù)的情況下,我們可以通過添加這個注解來避免生成型變泛型參數(shù)。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// 生成的代碼相當于下面這段Java代碼
Base unboxBase(Box<Base> box) { …… }
正確使用上述注解,可以抹平Kotlin與Java泛型處理的差異,避免出現(xiàn)安全轉換問題。
@Volatile @Transient
這兩個注解恰好對應Java端的兩個關鍵字volatile與transient,前者主要用于解決多線程臟數(shù)據(jù)問題,后者用于標記序列化對象中不參與序列化的屬性。
這兩個注解比較簡單,就不舉例說明了。在遇到類似需要與Java互通的場景時,只需要將其關鍵字替換為該注解即可。
以上就是我們日常開發(fā)過程中能夠遇到的所有注解了,在Kotlin 1.3版本中,還增加了一個新的注解@JvmDefault用于在接口中處理默認實現(xiàn)的方法。接口中允許有默認實現(xiàn)是從JDK 1.8版本開始的,為了兼容低版本JDK,Kotlin語言新增了該注解用于生成兼容性字節(jié)碼,但該注解目前仍處于實驗階段,名稱或行為均可能發(fā)生改變,建議大家先不要使用,推薦大家始終使用JDK 1.8及其以上版本。
最佳實踐
如果在工程中必須存在部分Java代碼,為了實現(xiàn)完美調用,一定要謹慎并正確地使用上述注解。要充分理解Kotlin編譯器與Java編譯器生成的字節(jié)碼差異。
如果是由于現(xiàn)存Java庫僅兼容Java字節(jié)碼,導致部分框架在遇到Kotlin語言生成的字節(jié)碼時會出現(xiàn)解析錯誤,不能正常使用。這個時候要嘗試檢查是否需要通過上述注解矯正字節(jié)碼的生成,使Java庫能夠正常使用。
如果是新工程,建議大家全部使用Kotlin代碼,避免出現(xiàn)上述注解,減少閱讀上的困難。目前,Kotlin版本已經(jīng)非常穩(wěn)定了,請大家放心使用。
閱讀更多技術文章,請關注微信公眾號”歐陽鋒工作室“
參與Kotlin技術討論,請?zhí)砑游ㄒ还俜絈Q交流群:329673958