正確使用Kotlin註解,兼容Java代碼

大多數狀況下,你不須要關注這個問題。可是,若是你的代碼中包含了部分Java代碼,理解這些註解將幫助你解決不少棘手問題。java

產生這個問題的根本緣由在於:Kotlin語言與Java語言的設計思路不一樣,部分特性屬於Java語言獨有,例如靜態變量。部分特性屬於Kotlin語言獨有,例如逆變和協變。編程

爲了抹平這些差別,Kotlin語言提供了一個絕佳的思路,經過添加註解能夠改變Kotlin編譯器生成的Java字節碼,使之按照Java語言能夠理解的方向進行,從而實現兼容。安全

問題答疑:Kotlin語言與Java字節碼有什麼關係?爲何Kotlin編譯器會生成Java字節碼?bash

不論是Kotlin語言仍是Java語言都是創建在JVM平臺上面的編程語言,其最終都須要編譯成JVM能夠識別的Java字節碼才能被正確執行。這也是爲何Kotlin語言與Java能夠徹底互通的緣由之一,不要將Java與Java平臺混爲一談。微信

接下來咱們先來看第一個註解,也是最經常使用到的一個註解:多線程

@JvmField

Kotlin編譯器默認會將類中聲明的成員變量編譯成私有變量,Java語言要訪問該變量必須經過其生成的getter方法。而使用上面的註解能夠向Java暴露該變量,即便其訪問變爲公開(修飾符變爲public)。併發

咱們來作一個實驗:框架

1)新建Person.kt,添加以下代碼:編程語言

class Person {
    @JvmField
    var name: String? = null
}
複製代碼

2)新建Client.java,添加以下代碼,嘗試訪問Person類中的變量nameui

public class Client {

    public static void main(String[] args) {
        Person p = new Person();
        // 在添加@JvmField註解以前,這樣訪問會報錯
        // 只能經過p.getName()的方式進行訪問
        String name = p.name;
    }
}
複製代碼

在添加@JvmField屬性前咱們試圖經過p.name的方式進行訪問,編譯器出現報錯。由於,默認生成的成員變量name是私有的。而添加該註解以後咱們竟然能夠正常訪問了。

因而可知,@JvmField註解的確使生成的字節碼發生了變化,咱們將字節碼用Java代碼來表示,具體發生的變化相似下面代碼發生的變化:

添加註解以前

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註解添加到普通變量上方,若是添加到伴隨對象的成員變量上方,會發生什麼呢?咱們來試試看:

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註解會使伴隨對象在伴生類中生成靜態成員變量,經過伴生類可直接對其進行訪問。

結論

@JvmField註解可改變字節碼的生成,其做用的目標是類成員變量或伴隨對象成員變量。做用在類成員中可以使該變量對外暴露,經過點語法直接訪問。即將私有成員變量公有化(public),並去掉setter/getter方法。做用在伴隨對象成員變量中,可使該伴隨對象中的變量生成在伴生對象中,成爲伴生對象的公有靜態成員變量,經過伴生類可直接訪問。

那麼問題來了,若是該註解做用在私有成員變量上方會發生什麼呢?請你們自行驗證。

@JvmStatic

這個註解與@JvmField很是容易出現混淆,二者均可以做用在伴隨對象成員變量上方,咱們來試試看,若是一樣做用在伴隨對象成員變量中,會出現什麼狀況。

添加@JvmField註解的效果,上面咱們已經看到了,咱們直接將註解修改成@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註解並無在伴生類中生成靜態的公有成員變量。第三種方式能夠正常訪問,證實該註解在伴生類中生成了靜態的公有getter方法。第二種方式能夠正常訪問,證實該註解不會破壞伴隨對象中原有成員的訪問方式。

由此,咱們能夠大膽猜想,@JvmStatic註解的做用應該是生成靜態的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的本質區別。

注意:因爲@JvmField不只會改變屬性的訪問權限,同時也會改變setter/getter方法的生成,細心的同窗應該已經注意到了。一旦添加了@JvmField註解,setter/getter方法也消失了(變量能夠經過點語法直接訪問,setter/getter方法也就不必存在了)。而@JvmStatic僅僅是使setter/getter方法變爲靜態方法,同時生成位置放置到伴生類中。這與@JvmField的處理方式有些衝突(@JvmField會直接刪除掉setter/getter方法)。爲了不衝突,Kotlin語言禁止將這兩個註解混淆使用。

以上是將@JvmStatic@JvmField做用在伴隨對象成員變量上的區別。實際上,@JvmStatic不只能夠修飾屬性(成員變量),還能夠修飾方法,修飾方法的做用與修飾屬性的做用一致,都是將方法變成靜態類型。

爲了更直觀地表示兩種的區別,咱們用一個表格完整展現兩個註解的區別:

註解 做用位置 做用
@JvmField 類屬性或對象屬性 使屬性修飾符成爲public
@JvmStatic 對象方法(包括伴生對象) 使用方法成爲靜態類型,若是做用在伴生對象方法中,其方法會成爲伴生類的靜態方法

@JvmName

這個註解能夠改變字節碼中生成的類名或方法名稱,若是做用在頂級做用域(文件中),則會改變生成對應Java類的名稱。若是做用在方法上,則會改變生成對應Java方法的名稱。

Test.kt

@file:JvmName("FooKt")

@JvmName("foo1")
fun foo() {
    println("Hello, Jvm...")
}
複製代碼

在Kotlin語言中,foo是一個全局方法,爲了兼容Java字節碼,實際會根據文件名生成對應的Java類TestKt.java,這是Kotlin編譯器的一個隱藏規則。

而添加了上述註解以後,生成的類名與方法名均發生了變化,具體產生的變化至關於下面這段Java代碼:

// 至關於下面的Java代碼
public final class FooKt {
   public static final void foo1() {
      String var0 = "Hello, Jvm...";
      System.out.println(var0);
   }
}
複製代碼

能夠看到第一個註解@file:JvmName("FooKt")的做用是使生成的類名變爲FooKt,第二個註解的做用是使生成的方法名稱變爲foo1

注意:該註解不能改變類中生成的屬性(成員變量)的名稱。

這裏的註解中,咱們看到了一個特殊的前綴@file:,這個註解前綴是Kotlin語言特有的一種標識,其做用是標記該註解最終會做用在生成的字節碼的具體位置(屬性、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,即生成的類名出現了重複。但是,若是咱們就是但願聲明使用多個文件,但方法生成到同一個類中呢?@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以後,報錯消失了,反編譯生成的字節碼,咱們發生兩個不一樣文件中的方法合併到了同一個類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);
   }
}
複製代碼

這個註解在處理多個文件聲明,合併到一個類的場景中發揮着舉足輕重的做用。若是你有這樣的需求,必定要謹記這個註解。

@JvmOverloads

因爲Kotlin語言支持方法參數默認值,而實現相似功能Java須要使用方法重載來實現,這個註解就是爲解決這個問題而生的,添加這個註解會自動生成重載方法。咱們來試一下:

@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);
}
複製代碼

因而可知,經過這個註解能夠影響帶有參數默認值方法的生成,添加該註解將自動生成帶有默認值參數數量的重載方法。這是一個很是有用的特性,方便Java端能夠更高效地調用Kotlin端代碼。

@Throws

因爲Kotlin語言不支持CE(Checked Exception),所謂CE,即方法可能拋出的異常是已知的。Java語言經過throws關鍵字在方法上聲明CE。爲了兼容這種寫法,Kotlin語言新增了@Throws註解,該註解的接收一個可變參數,參數類型是多個異常的KClass實例。Kotlin編譯器經過讀取註解參數,在生成的字節碼中自動添加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

這個註解很容易理解,顧名思義,主要用於產生同步方法。Kotlin語言不支持synchronized關鍵字,處理相似Java語言的併發問題,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

這個註解主要用於處理泛型參數,這涉及到兩個新的知識點:逆變協變。因爲Java語言不支持協變,爲了保證安全地相互調用,能夠經過在泛型參數聲明的位置添加該註解使用Kotlin編譯器生成通配符形式的泛型參數(?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語言中,泛型參數類型是不可變的,按照上面的寫法顯然已經作不到了。

正確轉換到Java代碼應該是這樣:

Base unboxBase(Box<? extends Base> box) { …… }
複製代碼

爲了使這樣的轉換正確生成,咱們須要在泛型參數的位置添加上面的註解:

fun unboxBase(box: Box<@JvmWildcard Base>): Base = box.value
複製代碼

@JvmSuppressWildcards

這個註解的做用與@JvmWildcard偏偏相反,它是用來抑制通配符泛型參數的生成,即在不須要型變泛型參數的狀況下,咱們能夠經過添加這個註解來避免生成型變泛型參數。

fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
複製代碼
// 生成的代碼至關於下面這段Java代碼
Base unboxBase(Box<Base> box) { …… }
複製代碼

正確使用上述註解,能夠抹平Kotlin與Java泛型處理的差別,避免出現安全轉換問題。

@Volatile @Transient

這兩個註解剛好對應Java端的兩個關鍵字volatiletransient,前者主要用於解決多線程髒數據問題,後者用於標記序列化對象中不參與序列化的屬性。

這兩個註解比較簡單,就不舉例說明了。在遇到相似須要與Java互通的場景時,只須要將其關鍵字替換爲該註解便可。

以上就是咱們平常開發過程當中可以遇到的全部註解了,在Kotlin 1.3版本中,還增長了一個新的註解@JvmDefault用於在接口中處理默認實現的方法。接口中容許有默認實現是從JDK 1.8版本開始的,爲了兼容低版本JDK,Kotlin語言新增了該註解用於生成兼容性字節碼,但該註解目前仍處於實驗階段,名稱或行爲都可能發生改變,建議你們先不要使用,推薦你們始終使用JDK 1.8及其以上版本。

最佳實踐

若是在工程中必須存在部分Java代碼,爲了實現完美調用,必定要謹慎並正確地使用上述註解。要充分理解Kotlin編譯器與Java編譯器生成的字節碼差別。

若是是因爲現存Java庫僅兼容Java字節碼,致使部分框架在遇到Kotlin語言生成的字節碼時會出現解析錯誤,不能正常使用。這個時候要嘗試檢查是否須要經過上述註解矯正字節碼的生成,使Java庫可以正常使用。

若是是新工程,建議你們所有使用Kotlin代碼,避免出現上述註解,減小閱讀上的困難。目前,Kotlin版本已經很是穩定了,請你們放心使用。

閱讀更多技術文章,請關注微信公衆號」歐陽鋒工做室「

參與Kotlin技術討論,請添加惟一官方QQ交流羣:329673958

相關文章
相關標籤/搜索