[Kotlin Tutorials 11] Kotlin和Java的雙向互操做

Kotlin和Java的雙向互操做

Kotlin和Java是有互操做性的(Interoperability). Kotlin和Java代碼能夠互相調用.html

爲何在一個項目裏這兩種語言會同時存在呢?java

  • 改造Java項目遷移到Kotlin時, 漸進改動就會有兩種語言同時存在, 相互調用的狀況.
  • 即使你選擇了一種語言, 極可能也須要用到庫是用另外一種語言寫的. 好比新寫一個Kotlin項目, 可是用到的庫仍然是Java的.

Kotlin調用Java

空安全

由於Java中的全部引用都是可能爲null的. Java聲明的類型在Kotlin中被稱爲platform types.安全

從Java中傳過來的引用, 能夠賦值給Kotlin的非空類型, 不會有編譯錯誤, 可是若是引用值爲空, 會在運行時拋出異常.bash

舉例, 在kotlin中調用Java的方法, 返回一個String:app

val stringOne = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)
複製代碼

若是Java方法返回null怎麼辦? 運行這段程序時先打出null, 再拋出NullPointerException.jvm

若是程序是這樣寫的:ide

val stringOne: String = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)
複製代碼

說明Kotlin假設Java傳回來的是一個非空值. 這段代碼編譯時不會報錯, 可是運行時第一行就拋出IllegalStateException.函數

若是這樣寫:測試

val stringOne: String? = JavaUtils.getStringOne()
println(stringOne)
println(stringOne?.length)
複製代碼

終於利用上了Kotlin的空安全檢查, 全部用到這個變量的地方都要加上?, 若是不作檢查編譯時就會提示錯誤. 可是這樣防護不免致使代碼太囉嗦了, 可能處處都是?.ui

好的實踐是Java中的Public APIs(Non-primitive parameters, Field type, Return)都應該加上註解.

若是Java的類型上有關於null的註解, 就會直接表示爲Kotlin中爲不爲null或者可爲null的對應類型.

註解能夠來自於各類包中, 好比JetBrains提供的: @Nullable@NotNull.

好比:

@NotNull
public static String getStringOne() {
    return "hello";
}
複製代碼

這樣Kotlin代碼就知道傳過來的確定是個非空值, 能夠放心使用.

若是是@Nullable, 編譯器就會提示使用前作檢查.

轉義在Kotlin中做爲關鍵字的Java標識符

Kotlin中的關鍵字, 好比:

fun, in, is, object, typealias, typeof, val, var, when
複製代碼

若是Java代碼中用了這些關鍵字, 在Kotlin中調用該Java代碼就要用`進行轉義.

好比若是java中有一個名稱爲is的方法, 在kotlin中想要調用:

foo.`is`(bar)
複製代碼

可是, 首先須要考慮是否是名字起得很差, 若是能夠更名(不是第三方代碼), 優先考慮更名.

比較常見的一個使用情形是在寫測試的時候, Mockito中的when就須要轉義:

Mockito.`when`(xxx.foo()).thenReturn(yyy)
複製代碼

由於Mockito是一個Java的第三方庫, 咱們無法改它.

另外一個解決辦法是使用import alias, 給這個方法取個別名:

import org.mockito.Mockito.`when` as whenever
複製代碼

這樣在使用的時候就能夠用whenever來代替了when了. import alias一般用來解決命名衝突的問題.

SAM Conversions

SAM: Single Abstract Method.

只要函數參數匹配, Kotlin的函數能夠自動轉換爲Java的接口實現.

Convention: 能夠作SAM轉換的參數類型應該放在方法的最後, 這樣看起來更舒服.

舉例, 若是在Java中定義方法:

interface Operation {
    int doCalculate(int left, int right);
}

public static int calculate(Operation operation, int firstNumber, int secondNumber) {
    return operation.doCalculate(firstNumber, secondNumber);
}
複製代碼

在Kotlin中調用的時候用SAM轉換, 用一個lambda做爲接口實現:

JavaUtils.calculate({ number1, number2 -> number1 + number2 }, 2, 3)
複製代碼

這樣雖然正確, 可是能夠改進. 把Java方法定義中的參數位置交換一下, 把接口參數放在最後:

public static int calculate(int firstNumber, int secondNumber, Operation operation) {
    return operation.doCalculate(firstNumber, secondNumber);
}
複製代碼

在Kotlin中, 最後一個lambda參數能夠提取到括號外面:

JavaUtils.calculate(2, 3) { number1, number2 -> number1 + number2 }
複製代碼

這樣看起來更好.

注意: SAM conversion只應用於java interop.

上面的例子, 若是接口和方法是在Kotlin中定義的:

interface Operation2 {
    fun doCalculate(left: Int, right: Int): Int
}

fun calculate2(firstNumber: Int, secondNumber: Int, operation: Operation2): Int {
    return operation.doCalculate(firstNumber, secondNumber)
}
複製代碼

SAM conversions就不能用了, IDE會提示沒法識別. 調用這個方法時, 第三個參數必須寫成這種(匿名類的對象, 實現了接口):

calculate2(2, 3, object : Operation2 {
    override fun doCalculate(left: Int, right: Int): Int {
        return left + right
    }
})
複製代碼

這是由於在Kotlin的世界裏, 函數是第一公民.

若是把前面的方法參數改成function type:

fun calculate3(firstNumber: Int, secondNumber: Int, operation: (Int, Int) -> Int): Int {
    return operation.invoke(firstNumber, secondNumber)
}
複製代碼

就能夠像以前SAM conversions似的使用:

calculate3(2, 3) { number1, number2 -> number1 + number2 }
複製代碼

若是接口是在Java中定義, 可是接收參數的方法是Kotlin的方法:

fun calculate4(firstNumber: Int, secondNumber: Int, operation: JavaUtils.Operation): Int {
    return operation.doCalculate(firstNumber, secondNumber)
}
複製代碼

仍然是不能用SAM conversions, 由於這個方法仍然是能夠接受函數類型的參數的. 在Kotlin中調用:

calculate4(2, 3, object : JavaUtils.Operation {
    override fun doCalculate(left: Int, right: Int): Int {
        return left + right
    }
})
複製代碼

IDE會提示你簡化爲:

calculate4(2, 3, JavaUtils.Operation { left, right -> left + right })
複製代碼

注意這裏接口名稱不能省略.

是否是感受有點暈, 我把上面提到的幾個調用狀況寫在一塊兒:

// java function, java interface parameter
private fun trySAM1() {
    JavaUtils.calculate(2, 3) { number1, number2 -> number1 + number2 }
}

// kotlin function, kotlin interface parameter
private fun trySAM2() {
    calculate2(2, 3, object : Operation2 {
        override fun doCalculate(left: Int, right: Int): Int {
            return left + right
        }
    })
}

// kotlin function, function type parameter
private fun trySAM3() {
    calculate3(2, 3) { number1, number2 -> number1 + number2 }
}

// kotlin function, java interface parameter
private fun trySAM4() {
    calculate4(2, 3, JavaUtils.Operation { left, right -> left + right })
}

複製代碼

能夠互相比較一下, 看看區別.

Getter和Setter

Java中的getter和setter在Kotlin中會表現爲properties. 可是若是隻有setter, 不會做爲可見的property.

異常

Kotlin中全部的異常都是unchecked的, 因此若是調用的Java代碼有受檢異常, kotlin並不會強迫你處理.

其餘

Java中返回void的方法在Kotlin中會變成Unit.

java.lang.Object會變成Any, Any中的不少方法都是擴展方法.

Java沒有運算符重載, 可是Kotlin支持. (運算符重載容易存在過分使用的問題.)

Java調用Kotlin

屬性

Kotlin的屬性會被編譯成Java中的一個私有字段, 加上getter和setter方法.

若是想要做爲一個字段, 能夠加上@JvmField註解.

包級別的方法

若是在一個文件app.kt中定義方法, 包名是org.example, 會被編譯成Java的靜態方法, Java類的類名是org.example.AppKt.

應用場景舉例: 舊代碼中有一個Java的輔助類, 包含靜態方法:

public class Utils {
    public static int distanceBetween(int point1, int point2) {
        return point2 - point1;
    }
}
複製代碼

要把這個輔助類遷移到Kotlin代碼, 能夠新建一個Kotlin文件DistanceUtils.kt, 直接寫包級別的方法:

fun distanceBetween(point1: Int, point2: Int): Int {
    return point2 - point1
}
複製代碼

在Java中調用這個方法的時候:

DistanceUtilsKt.distanceBetween(7, 9);
複製代碼

若是原先的Java代碼中包含調用這個方法的地方太多, 又不想改全部的usage, 怎麼辦? -> 能夠經過註解@file:JvmmName("xxx")改變類名. 這樣原先Java代碼中調用的地方就避免了修改.

若是有兩個文件指定了相同的JvmName, 編譯會報錯. 能夠經過加上@file:JvmMultifileClass來解決. 這樣多個Kotlin文件中定義的輔助方法對於Java來講會統一到同一個類中.

實例字段

如你須要把kotlin的property做爲字段暴露出來, 能夠加上@JvmField註解.

適用的property: 有backing field, 沒有這些修飾符: private, open, override, const, 也不是代理屬性.

lateinit的屬性會自動暴露爲fields, 可見性和屬性的setter一致.

Kotlin的data class會自動生成getter/setter. 若是加上@JvmField, 會直接暴露這些字段.

能夠經過@get:JvmName("xxx")@set:JvmName("xxx")來定製getter和setter的名字.

靜態字段

Kotlin在有名字的object或者companion object中聲明的屬性, 將會編譯成靜態字段.

一般這些字段是private的, 不過也能夠經過如下幾種方式暴露:

  • @JvmField.
  • lateinit.
  • const.

靜態方法

前面提過, Kotlin包級別的方法會被編譯成靜態方法.

在object或companion object中聲明的方法, 默認是類中的實例方法. 好比:

class StaticMethodsDemoClass {
    companion object {
        fun sayHello() {
            println("hello")
        }
    }
}

object SingletonObject {
    fun sayWorld() {
        println("world")
    }
}
複製代碼

在Java中調用的時候:

StaticMethodsDemoClass.Companion.sayHello();
SingletonObject.INSTANCE.sayWorld();
複製代碼

若是給object或companion object中的方法加上@JvmStatic, 會生成一個靜態方法和一個實例方法. 調用的時候就能夠省略掉中間的CompanionINSTANCE關鍵字, 以類名直接調用靜態方法.

好比:

class StaticMethodsDemoClass {
    companion object {
        fun sayHello() {
            println("hello")
        }

        @JvmStatic
        fun sayHelloStatic() {
            println("hello")
        }
    }
}
複製代碼

調用的時候:

StaticMethodsDemoClass.Companion.sayHello();
//StaticMethodsDemoClass.sayHello(); // error

StaticMethodsDemoClass.Companion.sayHelloStatic(); // ok, but not necessary
StaticMethodsDemoClass.sayHelloStatic();
複製代碼

@JvmStatic也能夠用於屬性, 就會有靜態版本的getter和setter方法.

其餘

Kotlin方法支持默認參數, 在Java中只有所有參數的方法簽名纔是可見的. 若是你但願對Java暴露多個方法重載, 要給方法加上@JvmOverloads. 好比在Kotlin中寫一個自定義View的構造函數:

class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}
複製代碼

還能夠利用 @JvmName來給方法重命名. 由於在Kotlin中是擴展方法, 在Java中只是一個靜態方法, 名字可能不夠直觀.

Kotlin沒有checked exceptions. 若是想在Java中調用一個Kotlin方法, 幷包一個try-catch, 會報錯說沒有拋出這個異常.

能夠在Kotlin方法中加上註解, 好比@Throws(IOException::class).

Feature leak prevention

Kotlin方法的參數名, 在生成代碼中會做爲一個字符串出現, 從而不會被混淆, 有可能會泄漏. 因此不建議放敏感信息到參數名中.

相似的還有字段名, 擴展方法名.

能夠在proguard中加上

-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
    public static void checkParameterIsNotNull(...);
    public static void throwUninitializedPropertyAccessException(...);
}
複製代碼

來移除這些代碼. 可是建議在測試環境中仍然保留這些代碼, 以便有錯誤發生的時候可以快速發現.

Tools

IDE的自動轉換

Code -> Convert Java File to Kotlin File.

若是粘貼Java代碼到.kt文件, IDE會自動將所粘貼代碼轉換爲Kotlin代碼.

查看編譯成的Java代碼

在IDE裏面能夠顯示Kotlin Bytecode, 而後decompile, 顯示java代碼.

Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile.

參考

相關文章
相關標籤/搜索