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

Kotlin和Java的雙向互操作

本文收錄于: https://github.com/mengdd/KotlinTutorials

Kotlin和Java是有互操作性的(Interoperability). Kotlin和Java代碼可以互相調(diào)用.

為什么在一個(gè)項(xiàng)目里這兩種語言會(huì)同時(shí)存在呢?

  • 改造Java項(xiàng)目遷移到Kotlin時(shí), 漸進(jìn)改動(dòng)就會(huì)有兩種語言同時(shí)存在, 相互調(diào)用的情況.
  • 即便你選擇了一種語言, 很可能也需要用到庫是用另一種語言寫的. 比如新寫一個(gè)Kotlin項(xiàng)目, 但是用到的庫仍然是Java的.

Kotlin調(diào)用Java

空安全

因?yàn)镴ava中的所有引用都是可能為null的. Java聲明的類型在Kotlin中被稱為platform types.

從Java中傳過來的引用, 可以賦值給Kotlin的非空類型, 不會(huì)有編譯錯(cuò)誤, 但是如果引用值為空, 會(huì)在運(yùn)行時(shí)拋出異常.

舉例, 在kotlin中調(diào)用Java的方法, 返回一個(gè)String:

val stringOne = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)

如果Java方法返回null怎么辦?
運(yùn)行這段程序時(shí)先打出null, 再拋出NullPointerException.

如果程序是這樣寫的:

val stringOne: String = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)

說明Kotlin假設(shè)Java傳回來的是一個(gè)非空值. 這段代碼編譯時(shí)不會(huì)報(bào)錯(cuò), 但是運(yùn)行時(shí)第一行就拋出IllegalStateException.

如果這樣寫:

val stringOne: String? = JavaUtils.getStringOne()
println(stringOne)
println(stringOne?.length)

終于利用上了Kotlin的空安全檢查, 所有用到這個(gè)變量的地方都要加上?, 如果不做檢查編譯時(shí)就會(huì)提示錯(cuò)誤.
但是這樣防御難免導(dǎo)致代碼太啰嗦了, 可能到處都是?.

好的實(shí)踐是Java中的Public APIs(Non-primitive parameters, Field type, Return)都應(yīng)該加上注解.

如果Java的類型上有關(guān)于null的注解, 就會(huì)直接表示為Kotlin中為不為null或者可為null的對(duì)應(yīng)類型.

注解可以來自于各種包中, 比如JetBrains提供的: @Nullable@NotNull.

比如:

@NotNull
public static String getStringOne() {
    return "hello";
}

這樣Kotlin代碼就知道傳過來的肯定是個(gè)非空值, 可以放心使用.

如果是@Nullable, 編譯器就會(huì)提示使用前做檢查.

轉(zhuǎn)義在Kotlin中作為關(guān)鍵字的Java標(biāo)識(shí)符

Kotlin中的關(guān)鍵字, 比如:

fun, in, is, object, typealias, typeof, val, var, when

如果Java代碼中用了這些關(guān)鍵字, 在Kotlin中調(diào)用該Java代碼就要用`進(jìn)行轉(zhuǎn)義.

比如如果java中有一個(gè)名稱為is的方法, 在kotlin中想要調(diào)用:

foo.`is`(bar)

但是, 首先需要考慮是不是名字起得不好, 如果可以改名(不是第三方代碼), 優(yōu)先考慮改名.

比較常見的一個(gè)使用情形是在寫測(cè)試的時(shí)候, Mockito中的when就需要轉(zhuǎn)義:

Mockito.`when`(xxx.foo()).thenReturn(yyy)

因?yàn)镸ockito是一個(gè)Java的第三方庫, 我們沒法改它.

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

import org.mockito.Mockito.`when` as whenever

這樣在使用的時(shí)候就可以用whenever來代替了when了.
import alias通常用來解決命名沖突的問題.

SAM Conversions

SAM: Single Abstract Method.

只要函數(shù)參數(shù)匹配, Kotlin的函數(shù)可以自動(dòng)轉(zhuǎn)換為Java的接口實(shí)現(xiàn).

Convention: 可以做SAM轉(zhuǎn)換的參數(shù)類型應(yīng)該放在方法的最后, 這樣看起來更舒服.

舉例, 如果在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中調(diào)用的時(shí)候用SAM轉(zhuǎn)換, 用一個(gè)lambda作為接口實(shí)現(xiàn):

JavaUtils.calculate({ number1, number2 -> number1 + number2 }, 2, 3)

這樣雖然正確, 但是可以改進(jìn).
把Java方法定義中的參數(shù)位置交換一下, 把接口參數(shù)放在最后:

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

在Kotlin中, 最后一個(gè)lambda參數(shù)可以提取到括號(hào)外面:

JavaUtils.calculate(2, 3) { number1, number2 -> number1 + number2 }

這樣看起來更好.

注意: SAM conversion只應(yīng)用于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會(huì)提示無法識(shí)別.
調(diào)用這個(gè)方法時(shí), 第三個(gè)參數(shù)必須寫成這種(匿名類的對(duì)象, 實(shí)現(xiàn)了接口):

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

這是因?yàn)樵贙otlin的世界里, 函數(shù)是第一公民.

如果把前面的方法參數(shù)改為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中定義, 但是接收參數(shù)的方法是Kotlin的方法:

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

仍然是不能用SAM conversions, 因?yàn)檫@個(gè)方法仍然是可以接受函數(shù)類型的參數(shù)的.
在Kotlin中調(diào)用:

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

IDE會(huì)提示你簡(jiǎn)化為:

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

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

是不是感覺有點(diǎn)暈, 我把上面提到的幾個(gè)調(diào)用情況寫在一起:

// 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 })
}

可以互相比較一下, 看看區(qū)別.

Getter和Setter

Java中的getter和setter在Kotlin中會(huì)表現(xiàn)為properties. 但是如果只有setter, 不會(huì)作為可見的property.

異常

Kotlin中所有的異常都是unchecked的, 所以如果調(diào)用的Java代碼有受檢異常, kotlin并不會(huì)強(qiáng)迫你處理.

其他

Java中返回void的方法在Kotlin中會(huì)變成Unit.

java.lang.Object會(huì)變成Any, Any中的很多方法都是擴(kuò)展方法.

Java沒有運(yùn)算符重載, 但是Kotlin支持. (運(yùn)算符重載容易存在過度使用的問題.)

Java調(diào)用Kotlin

屬性

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

如果想要作為一個(gè)字段, 可以加上@JvmField注解.

包級(jí)別的方法

如果在一個(gè)文件app.kt中定義方法, 包名是org.example, 會(huì)被編譯成Java的靜態(tài)方法, Java類的類名是org.example.AppKt.

應(yīng)用場(chǎng)景舉例: 舊代碼中有一個(gè)Java的輔助類, 包含靜態(tài)方法:

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

要把這個(gè)輔助類遷移到Kotlin代碼, 可以新建一個(gè)Kotlin文件DistanceUtils.kt, 直接寫包級(jí)別的方法:

fun distanceBetween(point1: Int, point2: Int): Int {
    return point2 - point1
}

在Java中調(diào)用這個(gè)方法的時(shí)候:

DistanceUtilsKt.distanceBetween(7, 9);

如果原先的Java代碼中包含調(diào)用這個(gè)方法的地方太多, 又不想改所有的usage, 怎么辦? ->
可以通過注解@file:JvmmName("xxx")改變類名. 這樣原先Java代碼中調(diào)用的地方就避免了修改.

如果有兩個(gè)文件指定了相同的JvmName, 編譯會(huì)報(bào)錯(cuò). 可以通過加上@file:JvmMultifileClass來解決. 這樣多個(gè)Kotlin文件中定義的輔助方法對(duì)于Java來說會(huì)統(tǒng)一到同一個(gè)類中.

實(shí)例字段

如你需要把kotlin的property作為字段暴露出來, 可以加上@JvmField注解.

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

lateinit的屬性會(huì)自動(dòng)暴露為fields, 可見性和屬性的setter一致.

Kotlin的data class會(huì)自動(dòng)生成getter/setter. 如果加上@JvmField, 會(huì)直接暴露這些字段.

可以通過@get:JvmName("xxx")@set:JvmName("xxx")來定制getter和setter的名字.

靜態(tài)字段

Kotlin在有名字的object或者companion object中聲明的屬性, 將會(huì)編譯成靜態(tài)字段.

通常這些字段是private的, 不過也可以通過以下幾種方式暴露:

  • @JvmField.
  • lateinit.
  • const.

靜態(tài)方法

前面提過, Kotlin包級(jí)別的方法會(huì)被編譯成靜態(tài)方法.

在object或companion object中聲明的方法, 默認(rèn)是類中的實(shí)例方法. 比如:

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

object SingletonObject {
    fun sayWorld() {
        println("world")
    }
}

在Java中調(diào)用的時(shí)候:

StaticMethodsDemoClass.Companion.sayHello();
SingletonObject.INSTANCE.sayWorld();

如果給object或companion object中的方法加上@JvmStatic, 會(huì)生成一個(gè)靜態(tài)方法和一個(gè)實(shí)例方法.
調(diào)用的時(shí)候就可以省略掉中間的CompanionINSTANCE關(guān)鍵字, 以類名直接調(diào)用靜態(tài)方法.

比如:

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

        @JvmStatic
        fun sayHelloStatic() {
            println("hello")
        }
    }
}

調(diào)用的時(shí)候:

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

StaticMethodsDemoClass.Companion.sayHelloStatic(); // ok, but not necessary
StaticMethodsDemoClass.sayHelloStatic();

@JvmStatic也可以用于屬性, 就會(huì)有靜態(tài)版本的getter和setter方法.

其他

Kotlin方法支持默認(rèn)參數(shù), 在Java中只有全部參數(shù)的方法簽名才是可見的. 如果你希望對(duì)Java暴露多個(gè)方法重載, 要給方法加上@JvmOverloads.
比如在Kotlin中寫一個(gè)自定義View的構(gòu)造函數(shù):

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

還可以利用
@JvmName來給方法重命名. 因?yàn)樵贙otlin中是擴(kuò)展方法, 在Java中只是一個(gè)靜態(tài)方法, 名字可能不夠直觀.

Kotlin沒有checked exceptions.
如果想在Java中調(diào)用一個(gè)Kotlin方法, 并包一個(gè)try-catch, 會(huì)報(bào)錯(cuò)說沒有拋出這個(gè)異常.

可以在Kotlin方法中加上注解, 比如@Throws(IOException::class).

Feature leak prevention

Kotlin方法的參數(shù)名, 在生成代碼中會(huì)作為一個(gè)字符串出現(xiàn), 從而不會(huì)被混淆, 有可能會(huì)泄漏. 所以不建議放敏感信息到參數(shù)名中.

類似的還有字段名, 擴(kuò)展方法名.

可以在proguard中加上

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

來移除這些代碼.
但是建議在測(cè)試環(huán)境中仍然保留這些代碼, 以便有錯(cuò)誤發(fā)生的時(shí)候能夠快速發(fā)現(xiàn).

Tools

IDE的自動(dòng)轉(zhuǎn)換

Code -> Convert Java File to Kotlin File.

如果粘貼Java代碼到.kt文件, IDE會(huì)自動(dòng)將所粘貼代碼轉(zhuǎn)換為Kotlin代碼.

查看編譯成的Java代碼

在IDE里面可以顯示Kotlin Bytecode, 然后decompile, 顯示java代碼.

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

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容